﻿<#
原作者: Shupershuff
汉化+国服增强：萌爸奈特李

Usage:
乐于见到你修改这个脚本以满足你的需求, 提供:
- 任何修改后的脚本都不会出售.
- 任何修改后的脚本在线发布都应该始终是开源的
- 任何修改后的脚本都不会被修改以启用或协助任何游戏更改或恶意行为包括 (但不仅限于): 可封禁的 Mods, 作弊, 漏洞, 钓鱼

Purpose:
	脚本 允许打开多个 D2R 实例并自动关闭 'DiabloII Check For Other Instances' 句柄
	脚本 从 csv 读取账号配置. 或者你可以运行脚本参数: -AccountUsername, -PW, -Region, -All, -Batch, -ManualSettingSwitcher

Notes:
- 多次失败尝试 (例如错误密码) 通过这个方法登录到特定区域可能会暂时锁定你. 如果你遇到这种情况, 你应该仍然能够通过战网客户端登录.

Servers:
 NA - us.actual.battle.net
 EU - eu.actual.battle.net
 KR - kr.actual.battle.net
 CN - cn.actual.battlenet.cn
#>

param($AccountUsername,$PW,$Region,$All,$Batch,$ManualSettingSwitcher,$Close,[switch]$TestTTS,[string]$TestVoice) #用于获取传入的参数
$CurrentVersion = "1.16.0"
###########################################################################################################################################
# Script itself
###########################################################################################################################################
$host.ui.RawUI.WindowTitle = "Diablo 2 Resurrected Loader"
if (($Null -ne $PW -or $Null -ne $AccountUsername) -and ($Null -ne $Batch -or $Null -ne $All)){#如果传入不兼容的参数, 优先 $All 和 $Batch (按顺序).
	$PW = $Null
	$AccountUsername = $Null
	if ($Null -ne $Batch -and $Null -ne $All){
		$Batch = $Null
	}
}
if ($Null -ne $AccountUsername){
	$ScriptArguments = "-accountusername $AccountUsername" #以管理员模式重启时回传之前的参数
}
if ($Null -ne $PW){
	$ScriptArguments += " -PW $PW"
}
if ($Null -ne $Region){
	$ScriptArguments += " -region $Region"
}
if ($Null -ne $All){
	$ScriptArguments += " -all $All"
	$Script:OpenAllAccounts = $true
}
if ($Null -ne $Batch){
	$ScriptArguments += " -batch $Batch"
	$Script:OpenBatches = $True
}
if ($Null -ne $ManualSettingSwitcher){
	$ScriptArguments += " -ManualSettingSwitcher $ManualSettingSwitcher"
	$Script:AskForSettings = $True
}
if ($Null -ne $Close){
	$ScriptArguments += " -close $Close"
}
if ($TestTTS){
	$ScriptArguments += " -TestTTS"
}
if ($Null -ne $TestVoice -and $TestVoice -ne ""){
	$ScriptArguments += " -TestVoice $TestVoice"
}
#检查是否使用了参数
if ($Null -ne $ScriptArguments){
	$Script:ParamsUsed = $true
}
Else {
	$Script:ParamsUsed = $false
}
	#以管理员模式运行脚本（可选 Windows Terminal）
	if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
		try { $wt = Get-Command wt.exe -ErrorAction SilentlyContinue } catch { $wt = $null }
		$useWT = ($Script:PreferredTerminal -eq "WT" -and $null -ne $wt)
		if ($useWT){ Start-Process $wt.Source "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" $ScriptArguments" -Verb RunAs }
		else { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" $ScriptArguments" -Verb RunAs }
		exit
	}

#$DebugMode = $True # 调试模式开关 将此行前的 # 去掉可启用debug
if ($DebugMode -eq $True){
	$DebugPreference = "Continue"
	$VerbosePreference = "Continue"
}

#设置窗口大小
[console]::WindowWidth=85; #脚本设计围绕这个宽度. 调整风险自负.
$WindowHeight=50 #可以调整, 但不要小于 42
do {
	Try{
		[console]::WindowHeight = $WindowHeight;
		$HeightSuccessfullySet = $True
	}
	Catch {
		$WindowHeight --
	}
} Until ($HeightSuccessfullySet -eq $True)
[console]::BufferWidth=[console]::WindowWidth
#set misc vars
$Script:X = [char]0x1b #escape character for ANSI text colors
$ProgressPreference = "SilentlyContinue"
$Script:WorkingDirectory = ((Get-ChildItem -Path $PSScriptRoot)[0].fullname).substring(0,((Get-ChildItem -Path $PSScriptRoot)[0].fullname).lastindexof('\')) #Set Current Directory path.
$Script:StartTime = Get-Date #Used for elapsed time. Is reset when script refreshes.
$Script:MOO = "%%%"
$Script:JobIDs = @()
$MenuRefreshRate = 30 #How often the script refreshes in seconds. This should be set to 30, don't change this please.
$Script:ScriptFileName = Split-Path $MyInvocation.MyCommand.Path -Leaf #find the filename of the script in case a user renames it.
$Script:SessionTimer = 0 #set initial session timer to avoid errors in info menu.
$Script:NotificationHasBeenChecked = $False
#Baseline of acceptable characters for ReadKey functions. Used to prevents receiving inputs from folk who are alt tabbing etc.
$Script:AllowedKeyList = @(48,49,50,51,52,53,54,55,56,57) #0 to 9
$Script:AllowedKeyList += @(96,97,98,99,100,101,102,103,104,105) #0 to 9 on numpad
$Script:AllowedKeyList += @(65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90) # A to Z
$Script:MenuOptions = @(65,66,67,68,71,73,74,79,82,83,84,88) #a, b, c, d, g, i, j, o, r, s, t and x. Used to detect singular valid entries where script can have two characters entered.
$Script:RegionCNMap = @{ CN = "国服"; KR = "亚服"; NA = "美服"; EU = "欧服" }
$Script:RegionOrder = @{ CN = 0; KR = 1; NA = 2; EU = 3 }
$EnterKey = 13
try {
	$SavedGamesRoot = ((Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" -name "{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}" -ErrorAction Stop)."{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}" + "\") # Saved Games 根目录
}
Catch {
	$SavedGamesRoot = ("C:\Users\" + $Env:UserName + "\Saved Games\")
}
$Script:SettingsProfilePathGlobal = ($SavedGamesRoot + "Diablo II Resurrected\")
$Script:SettingsProfilePathCN = ($SavedGamesRoot + "Diablo II Resurrected (CN)\")
# 兼容旧逻辑：默认指向存在的目录；若都不存在则先设为非 CN 路径，后续按区服再动态选择
if (Test-Path $Script:SettingsProfilePathGlobal) {
	$Script:SettingsProfilePath = $Script:SettingsProfilePathGlobal
} elseif (Test-Path $Script:SettingsProfilePathCN) {
	$Script:SettingsProfilePath = $Script:SettingsProfilePathCN
} else {
	$Script:SettingsProfilePath = $Script:SettingsProfilePathGlobal
}
Function RemoveMaximiseButton { # 删除脚本中的最大化按钮, 因为有时候我误点击最大化而不是最小化, 这让我很恼火. 直接从 ChatGPT 复制过来的.
	Add-Type @"
		using System;
		using System.Runtime.InteropServices;
		public class WindowAPI {
			public const int GWL_STYLE = -16;
			public const int WS_MAXIMIZEBOX = 0x10000;
			public const int WS_THICKFRAME = 0x40000;  // Window has a sizing border
			[DllImport("user32.dll")]
			public static extern IntPtr GetForegroundWindow();
			[DllImport("user32.dll", SetLastError = true)]
			public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
			[DllImport("user32.dll", SetLastError = true)]
			public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
		}
"@
	# 获取当前窗口的句柄 (PowerShell 窗口)
	$hWnd = [WindowAPI]::GetForegroundWindow()
	# 获取当前窗口样式
	$style = [WindowAPI]::GetWindowLong($hWnd, [WindowAPI]::GWL_STYLE)
	# 禁用最大化按钮通过删除 WS_MAXIMIZEBOX 样式. 也禁用调整宽度.
	$newStyle = $style -band -bnot ([WindowAPI]::WS_MAXIMIZEBOX -bor [WindowAPI]::WS_THICKFRAME)
	[WindowAPI]::SetWindowLong($hWnd, [WindowAPI]::GWL_STYLE, $newStyle) | out-null
}
Function IdleTimeChecker { #获取 Windows 空闲时间. 用于可选功能 "当很久没有输入时, 停止游戏计时器". 为我添加这个功能, 以防我 挂机DC 或 AFK 时仍在计时.
	if (-not ("IdleTime" -as [type])){
		Add-Type @"
		using System;
		using System.Runtime.InteropServices;
		public static class IdleTime {
			[StructLayout(LayoutKind.Sequential)]
			public struct LASTINPUTINFO {
				public uint cbSize;
				public uint dwTime;
			}
			[DllImport("user32.dll")]
			public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
			public static uint GetIdleTime() {
				LASTINPUTINFO lii = new LASTINPUTINFO(); //Get the Last Input Info (LII)
				lii.cbSize = (uint)Marshal.SizeOf(lii);
				GetLastInputInfo(ref lii);
				return ((uint)Environment.TickCount - lii.dwTime) / 1000; // This returns idle time in seconds. We'll calculate to minutes elswhere.
			}
		}
"@
	}
	return [IdleTime]::GetIdleTime() #when function is called, return the seconds since last keyboard/mouse activity was detected so we can see if user is active/idle in case user wants to pause time tracking stats.
}
Function ReadKey([string]$message=$Null,[bool]$NoOutput,[bool]$AllowAllKeys){#用于接收用户输入
	$key = $Null
	$Host.UI.RawUI.FlushInputBuffer()
	if (![string]::IsNullOrEmpty($message)){
		Write-Host -NoNewLine $message
	}
	$AllowedKeyList = $Script:AllowedKeyList + @(13,27) #添加 Enter & Escape 到允许键列表作为可接受的输入.
	while ($Null -eq $key){
	if ($Host.UI.RawUI.KeyAvailable){
			$key_ = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown,IncludeKeyUp")
			if ($True -ne $AllowAllKeys){
				if ($key_.KeyDown -and $key_.VirtualKeyCode -in $AllowedKeyList){
					$key = $key_
				}
			}
			else {
				if ($key_.KeyDown){
					$key = $key_
				}
			}
		}
		else {
			Start-Sleep -m 200  # 毫秒
		}
	}
	if ($key_.VirtualKeyCode -ne $EnterKey -and -not ($Null -eq $key) -and [bool]$NoOutput -ne $true){
		Write-Host ("$X[38;2;255;165;000;22m" + "$($key.Character)" + "$X[0m") -NoNewLine
	}
	if (![string]::IsNullOrEmpty($message)){
		Write-Host "" # newline
	}
	return $(
		if ($Null -eq $key -or $key.VirtualKeyCode -eq $EnterKey){
			""
		}
		ElseIf ($key.VirtualKeyCode -eq 27){ #if key pressed was escape
			"Esc"
		}
		else {
			$key.Character
		}
	)
}
Function ReadKeyTimeout([string]$message=$Null, [int]$timeOutSeconds=0, [string]$Default=$Null, [object[]]$AdditionalAllowedKeys = $null, [bool]$TwoDigitAcctSelection = $False){
	$key = $Null
	$inputString = ""
	$Host.UI.RawUI.FlushInputBuffer()
	if (![string]::IsNullOrEmpty($message)){
		Write-Host -NoNewLine $message
	}
	$Counter = $timeOutSeconds * 1000 / 250
	$AllowedKeyList = $Script:AllowedKeyList + $AdditionalAllowedKeys #添加任何其他指定的允许键输入 (例如 Enter).
	while ($Null -eq $key -and ($timeOutSeconds -eq 0 -or $Counter-- -gt 0)){
		if ($TwoDigitAcctSelection -eq $True -and $inputString.length -ge 1){
			$AllowedKeyList = $AllowedKeyList + 13 + 8 # 允许 enter 和 backspace 如果 1 个字符被输入.
		}
		if (($timeOutSeconds -eq 0) -or $Host.UI.RawUI.KeyAvailable){
			$key_ = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown,IncludeKeyUp")
			if ($key_.KeyDown -and $key_.VirtualKeyCode -in $AllowedKeyList){
				if ($key_.VirtualKeyCode -eq [System.ConsoleKey]::Backspace){
					$Counter = $timeOutSeconds * 1000 / 250 #重置计数器
					if ($inputString.Length -gt 0){
						$inputString = $inputString.Substring(0, $inputString.Length - 1) #remove last added character/number from variable
						# 清除控制台中的最后一个字符
						$Host.UI.RawUI.CursorPosition = @{
							X = [Math]::Max($Host.UI.RawUI.CursorPosition.X - 1, 0)
							Y = $Host.UI.RawUI.CursorPosition.Y
						}
						Write-Host -NoNewLine " " #-ForegroundColor Black
						$Host.UI.RawUI.CursorPosition = @{
							X = [Math]::Max($Host.UI.RawUI.CursorPosition.X - 1, 0)
							Y = $Host.UI.RawUI.CursorPosition.Y
						}
					}
				}
				ElseIf ($TwoDigitAcctSelection -eq $True -and $key_.VirtualKeyCode -notin $Script:MenuOptions + 27){
					$Counter = $timeOutSeconds * 1000 / 250 #reset counter
					if ($key_.VirtualKeyCode -eq $EnterKey -or $key_.VirtualKeyCode -eq 27){
						break
					}
					$inputString += $key_.Character
					Write-Host ("$X[38;2;255;165;000;22m" + $key_.Character + "$X[0m") -nonewline
					if ($inputString.length -eq 2){#if 2 characters have been entered
						break
					}
				}
				else {
					$key = $key_
					$inputString = $key_.Character
				}
			}
		}
		else {
			Start-Sleep -m 250 # Milliseconds
		}
	}
	if ($Counter -le 0){
		if ($InputString.Length -gt 0){# if it timed out, revert to no input if one character was entered.
			$InputString = "" #remove last added character/number from variable
		}
	}
	if ($TwoDigitAcctSelection -eq $False -or ($TwoDigitAcctSelection -eq $True -and $key_.VirtualKeyCode -in $Script:MenuOptions)){
		Write-Host ("$X[38;2;255;165;000;22m" + "$inputString" + "$X[0m")
	}
	if (![string]::IsNullOrEmpty($message) -or $TwoDigitAcctSelection -eq $True){
		Write-Host "" # newline
	}
	Write-Host #prevent follow up text from ending up on the same line.
	return $(
		If ($key.VirtualKeyCode -eq $EnterKey -and $EnterKey -in $AllowedKeyList){
			""
		}
		ElseIf ($key.VirtualKeyCode -eq 27){ #if key pressed was escape
			"Esc"
		}
		ElseIf ($inputString.Length -eq 0){
			$Default
		}
		else {
			$inputString
		}
	)
}
Function PressTheAnyKey {#Used instead of Pause so folk can hit any key to continue
	Write-Host "  按任何键继续..." -nonewline
	readkey -NoOutput $True -AllowAllKeys $True | out-null
	Write-Host
}
Function PressTheAnyKeyToExit {#Used instead of Pause so folk can hit any key to exit
	Write-Host "  按任何键退出..." -nonewline
	readkey -NoOutput $True -AllowAllKeys $True | out-null
	remove-job * -force
	Exit
}
Function Red {
	process { Write-Host $_ -ForegroundColor Red }
}
Function Yellow {
	process { Write-Host $_ -ForegroundColor Yellow }
}
Function Green {
	process { Write-Host $_ -ForegroundColor Green }
}
Function NormalText {
	process { Write-Host $_ }
}
Function FormatFunction { # Used to get long lines formatted nicely within the CLI. Possibly the most difficult thing I've created in this script. Hooray for Regex!
	param (
		[string] $Text,
		[int] $Indents,
		[int] $SubsequentLineIndents,
		[switch] $IsError,
		[switch] $IsWarning,
		[switch] $IsSuccess
	)
	if ($IsError -eq $True){
		$Colour = "Red"
	}
	ElseIf ($IsWarning -eq $True){
		$Colour = "Yellow"
	}
	ElseIf ($IsSuccess -eq $True){
		$Colour = "Green"
	}
	Else {
		$Colour = "NormalText"
	}
	$MaxLineLength = 76
	If ($Indents -ge 1){
		while ($Indents -gt 0){
			$Indent += " "
			$Indents --
		}
	}
	If ($SubsequentLineIndents -ge 1){
		while ($SubsequentLineIndents -gt 0){
			$SubsequentLineIndent += " "
			$SubsequentLineIndents --
		}
	}
	$Text -split "`n" | ForEach-Object {
		$Line = " " + $Indent + $_
		$SecondLineDeltaIndent = ""
		if ($Line -match '^[\s]*-'){ #For any line starting with any preceding spaces and a dash.
			$SecondLineDeltaIndent = "  "
		}
		if ($Line -match '^[\s]*\d+\.\s'){ #For any line starting with any preceding spaces, a number, a '.' and a space. Eg "1. blah".
			$SecondLineDeltaIndent = "   "
		}
		Function Formatter ([string]$line){
			$pattern = "[\e]?[\[]?[`"-,`.!']?\b[\w\-,'`"]+(\S*)" # Regular expression pattern to find the last word including any trailing non-space characters. Also looks to include any preceding special characters or ANSI escape character.
			$WordMatches = [regex]::Matches($Line, $pattern) # Find all matches of the pattern in the string
			# Initialize variables to track the match with the highest index
			$highestIndex = -1
			$SelectedMatch = $Null
			$PatternLengthCount = 0
			$ANSIPatterns = "\x1b\[38;\d{1,3};\d{1,3};\d{1,3};\d{1,3};\d{1,3}m","\x1b\[0m","\x1b\[4m"
			ForEach ($WordMatch in $WordMatches){ # Iterate through each match (match being a block of characters, ie each word).
				ForEach ($ANSIPattern in $ANSIPatterns){ #iterate through each possible ANSI pattern to find any text that might have ANSI formatting.
					$ANSIMatches = $WordMatch.value | Select-String -Pattern $ANSIPattern -AllMatches
					ForEach ($ANSIMatch in $ANSIMatches){
						$Script:ANSIUsed = $True
						$PatternLengthCount = $PatternLengthCount + (($ANSIMatch.matches | ForEach-Object {$_.Value}) -join "").length #Calculate how many characters in the text are ANSI formatting characters and thus won't be displayed on screen, to prevent skewing word count.
					}
				}
				$matchIndex = $WordMatch.Index
				$matchLength = $WordMatch.Length
				$matchEndIndex = $matchIndex + $matchLength - 1
				if ($matchEndIndex -lt ($MaxLineLength + $PatternLengthCount)){# Check if the match ends within the first $MaxLineLength characters
					if ($matchIndex -gt $highestIndex){# Check if this match has a higher index than the current highest
						$highestIndex = $matchIndex # This word has a higher index and is the winner thus far.
						$SelectedMatch = $WordMatch
						$lastspaceindex = $SelectedMatch.Index + $SelectedMatch.Length - 1 #Find the index (the place in the string) where the last word can be used without overflowing the screen.
					}
				}
			}
			try {
				$script:chunk = $Line.Substring(0, $lastSpaceIndex + 1) #Chunk of text to print to screen. Uses all words from the start of $line up until $lastspaceindex so that only text that fits on a single line is printed. Prevents words being cut in half and prevents loss of indenting.
			}
			catch {
				$script:chunk = $Line.Substring(0, [Math]::Min(($MaxLineLength), ($Line.Length))) #If the above fails for whatever reason. Can't exactly remember why I put this in here but leaving it in to be safe LOL.
			}
		}
		Formatter $Line
		if ($Script:ANSIUsed -eq $True){ #if fancy pants coloured text (ANSI) is used, write out the first line. Check if ANSI was used in any overflow lines.
			do {
				$Script:ANSIUsed = $False
				Write-Output $Chunk | out-host #have to use out-host due to pipeline shenanigans and at this point was too lazy to do things properly :)
				$Line = " " + $SubsequentLineIndent + $Indent + $Line.Substring($chunk.Length).trimstart() #$Line is equal to $Line but without the text that's already been outputted.
				Formatter $Line
			} until ($Script:ANSIUsed -eq $False)
			if ($Chunk -ne " " -and $Chunk.lenth -ne 0){#print any remaining text.
				Write-Output $Chunk | out-host
			}
		}
		Else { #if line has no ANSI formatting.
			Write-Output $Chunk | &$Colour
		}
		$Line = $Line.Substring($chunk.Length).trimstart() #remove the string that's been printed on screen from variable.
		if ($Line.length -gt 0){ # I see you're reading my comment. How thorough of you! This whole function was an absolute mindf#$! to come up with and took probably 30 hours of trial, error and rage (in ascending order of frequency). Odd how the most boring of functions can take up the most time :)
			Write-Output ($Line -replace "(.{1,$($MaxLineLength - $($Indent.length) - $($SubsequentLineIndent.length) -1 - $($SecondLineDeltaIndent.length))})(\s+|$)", " $SubsequentLineIndent$SecondLineDeltaIndent$Indent`$1`n").trimend() | &$Colour
		}
	}
}
Function StripANSI {
    param(
        [string] $Text
    )
    if ($null -eq $Text) { return "" }
    # Remove ANSI SGR sequences like ESC[38;2;...m / ESC[0m / ESC[4m
    return ($Text -replace "\x1b\[[0-9;]*m", "")
}
Function GetDisplayWidth {
    param(
        [string] $Text
    )
    $s = StripANSI $Text
    if ([string]::IsNullOrEmpty($s)) { return 0 }
    $i = 0
    $w = 0
    while ($i -lt $s.Length) {
        $category = [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($s, $i)
        # Zero-width categories (combining/format/control) should not advance the column
        if ($category -eq [System.Globalization.UnicodeCategory]::NonSpacingMark -or
            $category -eq [System.Globalization.UnicodeCategory]::EnclosingMark -or
            $category -eq [System.Globalization.UnicodeCategory]::Format -or
            $category -eq [System.Globalization.UnicodeCategory]::Control) {
            # advance over this codepoint but don't add width
            if ([char]::IsHighSurrogate($s[$i]) -and ($i + 1 -lt $s.Length) -and [char]::IsLowSurrogate($s[$i+1])) { $i += 2 } else { $i++ }
            continue
        }
        $codePoint = [System.Char]::ConvertToUtf32($s, $i)
        $isWide = $false
        if ( (
                ($codePoint -ge 0x1100 -and $codePoint -le 0x115F) -or
                $codePoint -eq 0x2329 -or $codePoint -eq 0x232A -or
                ($codePoint -ge 0x2E80 -and $codePoint -le 0xA4CF) -or # CJK, Yi, etc.
                ($codePoint -ge 0xAC00 -and $codePoint -le 0xD7A3) -or # Hangul syllables
                ($codePoint -ge 0xF900 -and $codePoint -le 0xFAFF) -or # CJK compatibility ideographs
                ($codePoint -ge 0xFE10 -and $codePoint -le 0xFE19) -or
                ($codePoint -ge 0xFE30 -and $codePoint -le 0xFE6F) -or
                ($codePoint -ge 0xFF00 -and $codePoint -le 0xFF60) -or # Fullwidth forms
                ($codePoint -ge 0xFFE0 -and $codePoint -le 0xFFE6) -or
                ($codePoint -ge 0x1F300 -and $codePoint -le 0x1F64F) -or # Misc symbols & emoticons
                ($codePoint -ge 0x1F900 -and $codePoint -le 0x1F9FF)
            )) { $isWide = $true }
        if ($isWide) { $w += 2 } else { $w += 1 }
        if ([char]::IsHighSurrogate($s[$i]) -and ($i + 1 -lt $s.Length) -and [char]::IsLowSurrogate($s[$i+1])) { $i += 2 } else { $i++ }
    }
    return $w
}
Function PadRightDisplay {
    param(
        [string] $Text,
        [int] $TargetWidth
    )
    $curr = GetDisplayWidth $Text
    $delta = $TargetWidth - $curr
    if ($delta -gt 0) { return ($Text + (" " * $delta)) }
    return $Text
}
Function PadLeftDisplay {
    param(
        [string] $Text,
        [int] $TargetWidth
    )
    $curr = GetDisplayWidth $Text
    $delta = $TargetWidth - $curr
    if ($delta -gt 0) { return ((" " * $delta) + $Text) }
    return $Text
}
Function CommaSeparatedList {
	param (
		[object] $Values,
		[switch] $NoOr,
		[switch] $AndText
	)
	ForEach ($Value in $Values){ #write out each account option, comma separated but show each option in orange writing. Essentially output overly complicated fancy display options :)
		if ($Value -ne $Values[-1]){
			Write-Host "$X[38;2;255;165;000;22m$Value$X[0m" -nonewline
			if ($Value -ne $Values[-2]){Write-Host ", " -nonewline}
		}
		else {
			if ($Values.count -gt 1){
				$AndOr = "or"
				if ($AndText -eq $True){
					$AndOr = "and"
				}
				if ($NoOr -eq $False){
					Write-Host " $AndOr " -nonewline
				}
				Else {
					Write-Host ", " -nonewline
				}
			}
			Write-Host "$X[38;2;255;165;000;22m$Value$X[0m" -nonewline
		}
	}
}
Function GetEmuToken { #For connecting to D2Emu for TZ and/or DClone data
	write-host " 获取 D2Emu 连接信息 ..."
	Try {
		if ($PSVersionTable.psversion.major -ge 7){# Run a PowerShell 5.1 script from within PowerShell 7.x, for whatever reason no content is returned on PS 7.x. Identified in https://github.com/shupershuff/Diablo2RLoader/issues/51
			write-host "  获取 D2Emu AES key..."
			$AES = & "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -Command {(invoke-webrequest https://d2emu.com/api/v1/shupertoken/aes -TimeoutSec 5).content | convertfrom-json}
			write-host "  获取 D2Emu Token..."
			$EncryptedToken = & "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -Command {((invoke-webrequest https://d2emu.com/api/v1/shupertoken/token -TimeoutSec 5).content | convertfrom-json).token}
		}Else {
			write-host "  获取 D2Emu AES key..."
			$AES = invoke-restmethod https://d2emu.com/api/v1/shupertoken/aes -TimeoutSec 5
			write-host "  获取 D2Emu AES key..."
			$EncryptedToken = (invoke-restmethod https://d2emu.com/api/v1/shupertoken/token -TimeoutSec 5).token
		}
		$Key = $AES.key
		$IV = $AES.iv
		$bytes = [System.Convert]::FromBase64String($EncryptedToken);
		$aes = [System.Security.Cryptography.Aes]::Create();
		$utf8 = [System.Text.Encoding]::Utf8;
		$aes.Key = $utf8.GetBytes($key);
		$aes.IV  = $utf8.GetBytes($iv);
		$decryptor = $aes.CreateDecryptor();
		$unencryptedData = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
		$aes.Dispose();
		$Script:EmuToken = [System.Text.Encoding]::UTF8.GetString($unencryptedData); #Decrypted token
		write-host "  D2Emu 连接成功."
	}
	Catch {
		$Script:EmuOfflineMode = $True
		$Script:EmuToken = ""
		FormatFunction -IsError -indents 1 "`n`n获取 D2Emu.com 连接 token 失败, 可能是 D2Emu 接口挂了."
		if ($Script:Config.DCloneTrackerSource -eq "d2emu.com"){
			$D2EmuErrorText = "Terror Zones and DClone"
		}
		Else {
			$D2EmuErrorText = "Terror Zones"
		}
		FormatFunction -IsError -indents 1 "特性 【$D2EmuErrorText】 暂时禁用.`n"
		PressTheAnyKey
	}
}
Function CreateShortcut {# Create Shortcut Function
	param (
		[string]$shortcutPath,
		[string]$targetPath,
		[string]$arguments
	)
	$shell = New-Object -ComObject WScript.Shell
	$shortcut = $shell.CreateShortcut($shortcutPath)
	$shortcut.TargetPath = $targetPath
	$shortcut.Arguments = $arguments
	$shortcut.Save()
}
Function DisplayPreviousAccountOpened {
		Write-Host "之前打开的账号是:" -foregroundcolor yellow -backgroundcolor darkgreen
		$Lastopened = @(
			[pscustomobject]@{Account=$Script:AccountFriendlyName;region=$Script:LastRegion}
		)
		Write-Host " " -NoNewLine
		Write-Host ("Account:  " + $Lastopened.Account) -foregroundcolor yellow -backgroundcolor darkgreen
		Write-Host " " -NoNewLine
		Write-Host "Region:  " $Lastopened.Region -foregroundcolor yellow -backgroundcolor darkgreen
}
Function InitialiseCurrentStats {
	if ((Test-Path -Path "$Script:WorkingDirectory\Stats.csv") -ne $true){#Create Stats CSV if it doesn't exist
		$Null = {} | Select-Object "TotalGameTime","TimesLaunched","LastUpdateCheck","HighRunesFound","UniquesFound","SetItemsFound","RaresFound","MagicItemsFound","NormalItemsFound","Gems","CowKingKilled","PerfectGems" | Export-Csv "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation
		Write-Host " Stats.csv 已创建!"
	}
	do {
		Try {
			Write-Host " 导入 Stats.csv..."
			$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv" #Get current stats csv details
		}
		Catch {
			Write-Host " 无法导入 stats.csv. 文件损坏或丢失." -foregroundcolor red
		}
		if ($null -ne $CurrentStats){
			#Todo: In the Future add CSV validation checks
			$StatsCSVImportSuccess = $True
		}
		else {#Error out and exit if there's a problem with the csv.
				if ($StatsCSVRecoveryAttempt -lt 1){
					try {
						Write-Host " 尝试从备份恢复 stats.csv..." -foregroundcolor red
						Copy-Item -Path $Script:WorkingDirectory\Stats.backup.csv -Destination $Script:WorkingDirectory\Stats.csv -ErrorAction stop
						Write-Host " 恢复成功!" -foregroundcolor Green
						$StatsCSVRecoveryAttempt ++
						PressTheAnyKey
					}
					Catch {
						$StatsCSVImportSuccess = $False
					}
				}
				Else {
					$StatsCSVRecoveryAttempt = 2
				}
				if ($StatsCSVImportSuccess -eq $False -or $StatsCSVRecoveryAttempt -eq 2){
					Write-Host "`n Stats.csv 损坏或空." -foregroundcolor red
					Write-Host " 替换为 stats.backup.csv 或删除 stats.csv`n" -foregroundcolor red
					PressTheAnyKeyToExit
				}
			}
	} until ($StatsCSVImportSuccess -eq $True)
	if (-not ($CurrentStats | Get-Member -Name "LastUpdateCheck" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#For update 1.8.1+. If LastUpdateCheck column doesn't exist, add it to the CSV data
		$Script:CurrentStats | ForEach-Object {
			$_ | Add-Member -NotePropertyName "LastUpdateCheck" -NotePropertyValue "2000.06.28 12:00:00" #previously "28/06/2000 12:00:00 pm"
		}
	}
	ElseIf ($CurrentStats.LastUpdateCheck -eq "" -or $CurrentStats.LastUpdateCheck -like "*/*"){# If script has just been freshly downloaded or has the old Date format.
		$Script:CurrentStats.LastUpdateCheck = "2000.06.28 12:00:00" #previously "28/06/2000 12:00:00 pm"
		$CurrentStats | Export-Csv "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation
	}
}
Function ImportXML { #Import Config XML
	try {
		Write-Host " Importing Config.xml..."
		$Script:Config = ([xml](Get-Content "$Script:WorkingDirectory\Config.xml" -ErrorAction Stop)).D2loaderconfig
		Write-Verbose "Config imported successfully."
	}
	Catch {
		Write-Host "`n Config.xml Was not able to be imported. This could be due to a typo or a special character such as `'&`' being incorrectly used." -foregroundcolor red
		Write-Host " The error message below will show which line in the config.xml is invalid:" -foregroundcolor red
		Write-Host (" " + $PSitem.exception.message + "`n") -foregroundcolor red
		PressTheAnyKeyToExit
	}
}
Function SetDCloneAlarmLevels {
	if ($Script:Config.DCloneAlarmLevel -eq "All"){
		$Script:DCloneAlarmLevel = "1,2,3,4,5,6"
	}
	ElseIf ($Script:Config.DCloneAlarmLevel -eq "Close"){
		$Script:DCloneAlarmLevel = "1,4,5,6"
	}
	ElseIf ($Script:Config.DCloneAlarmLevel -eq "Imminent"){
		$Script:DCloneAlarmLevel = "1,5,6"
	}
	else {#if user has typo'd the config file or left it blank.
		$DCloneErrorMessage = ("  Error: DClone Alarm Levels have been misconfigured in config.xml. ###  Check that the value for DCloneAlarmLevel is entered correctly.").Replace("###", "`n")
		Write-Host ("`n" + $DCloneErrorMessage + "`n") -Foregroundcolor red
		PressTheAnyKeyToExit
	}
}
Function ValidationAndSetup {
	write-host  " 验证配置..."
	#对 config.xml进行一些验证. 帮助避免错误对于可能使用旧版本脚本的人.
	if (Select-String -path $Script:WorkingDirectory\Config.xml -pattern "multiple game installs"){#Sort out an incorrect description text that will have been in folks config.xml for some time. This description was never valid and was from when the setting switcher feature was being developed and tested.
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = ";;`t`t注意：如果复制了多份游戏客户端（为了保持每个账号的客户端特定配置持久化），请确保这些在 accounts.csv 的 CustomGamePath 字段中引用."
		$Pattern += ";;`t`t事实上更推荐使用一份客户端的方式，通过windows的 link 功能来创建多个.-->;;"
		$NewXML = [string]::join(";;",($XML.Split("`r`n")))
		$NewXML = $NewXML -replace $Pattern, "-->;;"
		$NewXML = $NewXML -replace ";;","`r`n"
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Write-Host "`n 修正 config.xml 中的 GamePath 描述." -foregroundcolor Green
		Start-Sleep -milliseconds 1500
	}
	if ($Null -ne $Script:Config.CommandLineArguments){#remove this config option as arguments are now stored in accounts.csv so that different arguments can be set for each account
		Write-Host "`n 配置项 'CommandLineArguments' 被移动到了 accounts.csv" -foregroundcolor Yellow
		Write-Host " 以支持为每个账号设置不同的命令行参数." -foregroundcolor Yellow
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = ";;\t<!--可选添加任何你希望游戏启动时使用的命令行参数-->;;\t<CommandLineArguments>.*?</CommandLineArguments>;;"
		$NewXML = [string]::join(";;",($XML.Split("`r`n")))
		$NewXML = $NewXML -replace $Pattern, ""
		$NewXML = $NewXML -replace ";;","`r`n"
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		$Script:OriginalCommandLineArguments = $Script:Config.CommandLineArguments
		Write-Host " CommandLineArguments 从 config.xml 中移除了" -foregroundcolor green
		Start-Sleep -milliseconds 1500
	}
	if ($Null -eq $Script:Config.ManualSettingSwitcherEnabled){#not to be confused with the AutoSettingSwitcher.
		Write-Host "`n 配置项 'ManualSettingSwitcherEnabled' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，允许你在启动时为每个账号指定独立的 config 文件 " -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</SettingSwitcherEnabled>"
		$Replacement = "</SettingSwitcherEnabled>`n`n`t<!--可以单独使用或与上面的标准设置切换器结合使用.`n`t"
		$Replacement +=	"这启用了菜单选项输入 's' 并手动选择设置配置文件. 如果你想要选择 Awesome 或 Poo 图形设置，则设置为 true.`n`t"
		$Replacement +=	"任何带有数字的设置文件将不会是一个可用的选项 (eg settings1.json 将不会是一个选项).`n`t"
		$Replacement +=	"要使设置选项可用, 你可以从 settings.<name>.json 文件中加载, 例如 (settings.Awesome Graphics.json) 将在菜单中显示为 `"Awesome Graphics`" .-->`n`t"
		$Replacement +=	"<ManualSettingSwitcherEnabled>False</ManualSettingSwitcherEnabled>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.TrackAccountUseTime){
		Write-Host "`n 设置项 'TrackAccountUseTime' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于跟踪每个账号的使用时间." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</ManualSettingSwitcherEnabled>"
		$Replacement = "</ManualSettingSwitcherEnabled>`n`n`t<!--这允许你大致跟踪使用这个脚本时每个账号的使用时间. "
		$Replacement += "选择 False 如果你想要禁用这个功能.-->`n`t<TrackAccountUseTime>True</TrackAccountUseTime>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.IdleLimitForAccountUseTime){
		Write-Host "`n 设置项 'IdleLimitForAccountUseTime' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于跟踪每个账号的使用时间." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</TrackAccountUseTime>"
		$Replacement = "</TrackAccountUseTime>`n`n`t<!--如果设置，则当你在一定时间内处于空闲状态时，会自动暂停时间跟踪. 设置为数字 (例如 5 表示 5 分钟). 默认设置为空 (禁用).-->"
		$Replacement += "`n`t<IdleLimitForAccountUseTime></IdleLimitForAccountUseTime>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DisableIconStacking){
		Write-Host "`n 设置项 'DisableIconStacking' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于禁用 D2r 图标在任务栏中的堆叠." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</ShortcutCustomIconPath>"
		$Replacement = "</ShortcutCustomIconPath>`n`n`t<!--如果启用，则此选项防止 D2r 图标在任务栏中堆叠. "
		$Replacement += "设置为 True 如果你想要启用这个功能.-->`n`t<DisableIconStacking>False</DisableIconStacking>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.ConvertPlainTextSecrets){
		Write-Host "`n 重命名 ConvertPlainTextPasswords 为 ConvertPlainTextSecrets 在 config.xml" -foregroundcolor Yellow
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "<!--Whether script should convert plain text passwords in accounts.csv to a secure string, recommend leaving this set to True.-->"
		$Replacement = "<!--是否应将 accounts.csv 中的纯文本密码转换为安全字符串，建议将此设置保留为 True.-->"
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$Pattern = "ConvertPlainTextPasswords"
		$Replacement = "ConvertPlainTextSecrets"
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Write-Host " config.xml 文件已更新 :)`n" -foregroundcolor green
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -ne $Script:Config.EnableBatchFeature){ # remove from config.xml. Not needed anymore as script checks accounts.csv to see if batches are used.
		Write-Host "`n 设置项 'EnableBatchFeature' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于启用批量功能." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = ";;\t<!--启用打开一组账号的功能.*?</EnableBatchFeature>;;"
		$NewXML = [string]::join(";;",($XML.Split("`r`n")))
		$NewXML = $NewXML -replace $Pattern, ""
		$NewXML = $NewXML -replace ";;","`r`n"
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -ne $Script:Config.CheckForNextTZ){
		Write-Host "`n 设置项 'CheckForNextTZ' 不再需要了." -foregroundcolor Yellow
		Write-Host " 将此设置项从 config.xml 中移除 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = ";;\t<!--Choose whether or not TZ checker.*?</CheckForNextTZ>;;"
		$NewXML = [string]::join(";;",($XML.Split("`r`n")))
		$NewXML = $NewXML -replace $Pattern, ""
		$NewXML = $NewXML -replace ";;","`r`n"
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -ne $Script:Config.AskForRegionOnceOnly){
		Write-Host "`n 设置项 'AskForRegionOnceOnly' 不再需要了." -foregroundcolor Yellow
		Write-Host " 将此设置项从 config.xml 中移除 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = ";;\t<!--Whether script should only prompt you once for region.*?</AskForRegionOnceOnly>;;"
		$NewXML = [string]::join(";;",($XML.Split("`r`n")))
		$NewXML = $NewXML -replace $Pattern, ""
		$NewXML = $NewXML -replace ";;","`r`n"
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DisableOpenAllAccountsOption){
		Write-Host "`n 设置项 'DisableOpenAllAccountsOption' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于禁用一键打开所有账号的功能." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DefaultRegion>"
		$Replacement = "</DefaultRegion>`n`n`t<!--禁用一键打开所有账号的功能. 这是为了防止意外打开所有账号.-->`n`t"
		$Replacement += "<DisableOpenAllAccountsOption>False</DisableOpenAllAccountsOption>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.RememberWindowLocations){
		Write-Host "`n 设置项 'RememberWindowLocations' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于记住游戏窗口位置/大小." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</ConvertPlainTextSecrets>"
		$Replacement = "</ConvertPlainTextSecrets>`n`n`t<!--让游戏每次启动时都在同一个屏幕位置，所以你不需要在启动游戏时移动你的游戏窗口.-->`n`t"
		$Replacement += "<RememberWindowLocations>False</RememberWindowLocations>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DCloneTrackerSource){
		Write-Host "`n 设置项 'DCloneTrackerSource' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个必需的配置项，用于确定应使用哪个源来获取当前 DClone 状态." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</TrackAccountUseTime>"
		$Replacement = "</TrackAccountUseTime>`n`n`t<!--选项是 d2emu.com, D2runewizard.com 和 diablo2.io.`n`t"
		$Replacement += "默认和推荐选项是 d2emu.com 因为这从游戏拉取实时数据而不是 crowdsourced data.-->`n`t"
		$Replacement += "<DCloneTrackerSource>d2emu.com</DCloneTrackerSource>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DCloneAlarmLevel){
		Write-Host "`n 设置项 'DCloneAlarmLevel' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于确定是否应激活所有 DClone 状态的警报." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DCloneTrackerSource>"
		$Replacement = "</DCloneTrackerSource>`n`n`t<!--指定你想要警报的状态.`n`t"
		$Replacement +=	"输入 `"All`" 来警报所有状态变化`n`t"
		$Replacement +=	"输入 `"Close`" 来只在状态是 4/6, 5/6 或刚刚走动时警报.`n`t"
		$Replacement +=	"输入 `"Imminent`" 来只在状态是 5/6 或刚刚走动时警报.`n`t"
		$Replacement +=	"推荐设置为 `"All`"-->`n`t"
		$Replacement +=	"<DCloneAlarmLevel>All</DCloneAlarmLevel>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DCloneAlarmList){
		Write-Host "`n 设置项 'DCloneAlarmList' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于启用音频和文本警报." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DCloneTrackerSource>"
		$Replacement = "</DCloneTrackerSource>`n`n`t<!--允许脚本大声警告你即将到来的 DClone 走动.`n`t"
		$Replacement +=	"指定你想要的选项: SCL-CN, SCL-NA, SCL-EU, SCL-KR, SC-CN, SC-NA, SC-EU, SC-KR, HCL-CN, HCL-NA, HCL-EU, HCL-KR, HC-CN, HC-NA, HC-EU, HC-KR`n`t"
		$Replacement +=	"例如：如果你想要被通知所有SoftCore Ladder在所有区域的黑毛事件, 输入 <DCloneAlarmList>SCL-CN,SCL-NA, SCL-EU, SCL-KR</DCloneAlarmList>`n`t"
		$Replacement +=	"如果留空，这个功能被禁用. 默认是空，因为这可能对一些人来说很烦人.-->`n`t"
		$Replacement +=	"<DCloneAlarmList></DCloneAlarmList>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		ImportXML
		PressTheAnyKey
	}
	if ($Null -ne $Script:Config.DCloneAlarmList -and $Script:Config.DCloneAlarmList -ne ""){#validate data to prevent errors from typos
		$pattern = "^(HC|SC)(L?)-(NA|EU|KR|CN)$" #set pattern: must start with HC or SC, optionally has L after it, must end in -NA -EU or -KR
		ForEach ($Alarm in $Script:Config.DCloneAlarmList.split(",").trim()){
			if ($Alarm -notmatch $pattern){
				Write-Host "`n $Alarm 不是一个有效的警报条目."  -foregroundcolor Red
				Write-Host " 请查看 config.xml 中的有效选项`n"  -foregroundcolor Red
				PressTheAnyKeyToExit
			}
		}
	}
	if ($Null -eq $Script:Config.DCloneAlarmVoice){
		Write-Host "`n 设置项 'DCloneAlarmVoice' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于选择一个女性或男性的机器人语音." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DCloneAlarmLevel>"
		$Replacement = "</DCloneAlarmLevel>`n`n`t<!--指定你想要的声音. 选择 'Paladin' 为 David (男) 或 'Amazon' 为 Zira (女).-->`n`t"
		$Replacement +=	"<DCloneAlarmVoice>Paladin</DCloneAlarmVoice>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.DCloneAlarmVolume){
		Write-Host "`n 设置项 'DCloneAlarmVolume' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于指定通知的音量." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DCloneAlarmVoice>"
		$Replacement = "</DCloneAlarmVoice>`n`t<!--指定通知的音量. 范围从 1 到 100.-->`n`t"
		$Replacement +=	"<DCloneAlarmVolume>69</DCloneAlarmVolume>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Null -eq $Script:Config.ForceAuthTokenForRegion){
		Write-Host "`n 设置项 'ForceAuthTokenForRegion' 在 config.xml 中缺失" -foregroundcolor Yellow
		Write-Host " 这是由于 config.xml 最近被更新." -foregroundcolor Yellow
		Write-Host " 这是一个可选的配置项，用于强制使用 AuthToken 认证." -foregroundcolor Yellow
		Write-Host " 在 config.xml 中指定的区域. 适用于当 Blizzard 的认证服务器在特定区域出现问题时." -foregroundcolor Yellow
		Write-Host " 添加了这个缺失的选项到 .xml 文件 :)`n" -foregroundcolor green
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
		$Pattern = "</DCloneAlarmVoice>"
		$Replacement = "</DCloneAlarmVoice>`n`n`t<!--选择哪些区域应该强制使用 Tokens 而不是参数 (覆盖 accounts.csv 中的配置).`n`t"
		$Replacement += "仅在连接 via Parameters 在特定区域出现问题时使用. 你不想手动切换.`n`t"
		$Replacement +=	"有效选项是 NA, EU, KR 和 CN. 默认是空.-->`n`t"
		$Replacement +=	"<ForceAuthTokenForRegion></ForceAuthTokenForRegion>" #add option to config file if it doesn't exist.
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	$XML = Get-Content "$Script:WorkingDirectory\Config.xml" -raw
	$Pattern = "d2rapi.fly.dev"
	if ($Null -ne (Select-Xml -Content $xml -XPath "//*[contains(.,'$pattern')]")){
		Write-Host "`n 替换 config 中的 'd2rapi.fly.dev' 为 'd2emu.com'." -foregroundcolor Yellow
		$Replacement = "d2emu.com"
		$NewXML = $XML -replace [regex]::Escape($Pattern), $Replacement
		$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
		Start-Sleep -milliseconds 1500
		PressTheAnyKey
	}
	if ($Script:Config.DCloneAlarmList -ne ""){
		$ValidVoiceOptions =
			"Amazon",
			"Paladin",
			"Woman",
			"Man",
			"Wench",
			"Bloke",
			"CN",
			"ZH",
			"ZH-CN",
			"ZHCN"
		if ($Script:Config.DCloneAlarmVoice -notin $ValidVoiceOptions){
			Write-Host "`n 错误: DCloneAlarmVoice 在 config 中有一个无效的选项设置." -Foregroundcolor red
			Write-Host " 打开 config.xml 并设置这个为 Paladin 或 Amazon.`n" -Foregroundcolor red
			PressTheAnyKeyToExit
		}
		SetDCloneAlarmLevels
	}
	$Script:Config = ([xml](Get-Content "$Script:WorkingDirectory\Config.xml" -ErrorAction Stop)).D2loaderconfig #import config.xml again for any updates made by the above.
	#Check if there's any missing config.xml options, if so user has an out of date config file.
	$AvailableConfigs = #add to this if adding features.
  "GamePath",
  "GamePathCN",
	"PreferredTerminal",
	"DefaultRegion",
	"ShortcutCustomIconPath",
	"DCloneTrackerSource",
	"DCloneAlarmList",
	"DCloneAlarmLevel",
	"DCloneAlarmVoice",
	"DCloneAlarmVolume",
	"ForceAuthTokenForRegion",
	"IdleLimitForAccountUseTime"
	$BooleanConfigs =
	"ConvertPlainTextSecrets",
	"ManualSettingSwitcherEnabled",
	"RememberWindowLocations",
	"DisableOpenAllAccountsOption",
	"CreateDesktopShortcut",
	"ForceWindowedMode",
	"SettingSwitcherEnabled",
	"TrackAccountUseTime",
	"DisableIconStacking"
	$AvailableConfigs = $AvailableConfigs + $BooleanConfigs
	$ConfigXMLlist = ($Config | Get-Member | Where-Object {$_.membertype -eq "Property" -and $_.name -notlike "#comment"}).name
	ForEach ($Option in $AvailableConfigs){#Config validation
		if ($Option -notin $ConfigXMLlist){
			Write-Host " Config.xml file 缺少配置项 $Option." -foregroundcolor yellow
			Start-Sleep 1
			PressTheAnyKey
		}
	}
	if ($Option -notin $ConfigXMLlist){
		Write-Host "`n 确保你使用了最新的 config.xml 版本" -foregroundcolor yellow
		PressTheAnyKey
	}
	if ($Config.GamePath -match "`""){#Remove any quotes from path in case someone ballses this up.
		$Script:GamePath = $Config.GamePath.replace("`"","")
	}
	else {
		$Script:GamePath = $Config.GamePath
	}
	# Parse optional CN client path
	if ($Config.GamePathCN -match "`""){
		$Script:GamePathCN = $Config.GamePathCN.replace("`"","")
	}
	else {
		$Script:GamePathCN = $Config.GamePathCN
	}
	ForEach ($ConfigCheck in $BooleanConfigs){#validate all configs that require "True" or "False" as the setting.
		if ($Null -ne $Config.$ConfigCheck -and ($Config.$ConfigCheck -ne $true -and $Config.$ConfigCheck -ne $false)){#if config is invalid
			Write-Host " Config option '$ConfigCheck' is invalid." -foregroundcolor Red
			Write-Host " Ensure this is set to either True or False.`n" -foregroundcolor Red
			PressTheAnyKeyToExit
		}
	}
	if ($Config.ShortcutCustomIconPath -match "`""){#Remove any quotes from path in case someone ballses this up.
		$ShortcutCustomIconPath = $Config.ShortcutCustomIconPath.replace("`"","")
	}
	else {
		$ShortcutCustomIconPath = $Config.ShortcutCustomIconPath
	}
	# Normalize preferred terminal
	$Script:PreferredTerminal = ($Config.PreferredTerminal | ForEach-Object { $_ })
	if ([string]::IsNullOrWhiteSpace($Script:PreferredTerminal)) { $Script:PreferredTerminal = "WT" }
	else { $Script:PreferredTerminal = $Script:PreferredTerminal.ToUpper() }
	#Check Windows Game Path(s) for D2r.exe
	$GlobalPathOK = $False
	$CNPathOK = $False
	$GlobalPathSet = ($Script:GamePath -and -not [string]::IsNullOrWhiteSpace($Script:GamePath))
	$CNPathSet = ($Script:GamePathCN -and -not [string]::IsNullOrWhiteSpace($Script:GamePathCN))
	try { if ($GlobalPathSet -and (Test-Path -Path "$Script:GamePath\d2r.exe")) { $GlobalPathOK = $True } } catch {}
	try { if ($CNPathSet -and (Test-Path -Path "$Script:GamePathCN\d2r.exe")) { $CNPathOK = $True } } catch {}
	if (-not ($GlobalPathSet -or $CNPathSet)){
		Write-Host " 至少需要配置一个客户端路径：GamePath（国际服）或 GamePathCN（国服）。" -ForegroundColor Red
		PressTheAnyKeyToExit
	}
	if ($GlobalPathSet -and -not $GlobalPathOK){
		Write-Host " 警告：已配置 GamePath，但未找到 D2R.exe。若不使用国际服，可将 GamePath 留空。" -ForegroundColor Yellow
	}
	if ($CNPathSet -and -not $CNPathOK){
		Write-Host " 警告：已配置 GamePathCN，但未找到 D2R.exe。若不使用国服，可将 GamePathCN 留空。" -ForegroundColor Yellow
	}
	if (-not ($GlobalPathOK -or $CNPathOK)){
		Write-Host " 未检测到可用的 D2R 客户端。请修正 GamePath 或 GamePathCN（可将不使用的一项留空）。" -ForegroundColor Red
		PressTheAnyKeyToExit
	}
	#Check IdleLimitForAccountUseTime is an integer
	if ($Config.IdleLimitForAccountUseTime -ne ""){#if this config option isn't blank and isn't number, have a big moan about it.
		try {
			$Script:IdleLimitForAccountUseTime = [int]$Config.IdleLimitForAccountUseTime  
		}
		Catch {
			Write-Host "`n Config Option 'IdleLimitForAccountUseTime' is invalid." -foregroundcolor red
			Write-Host " This should be a number." -foregroundcolor red
			Write-Host " Edit this variable to a number in the config.xml file.`n" -foregroundcolor red
			PressTheAnyKeyToExit
		}
		if ($Script:IdleLimitForAccountUseTime -eq 0){
			Write-Host "`n Config Option 'IdleLimitForAccountUseTime' is invalid." -foregroundcolor red
			Write-Host " This needs to be a number larger than 0." -foregroundcolor red
			Write-Host " Edit this variable to a number in the config.xml file.`n" -foregroundcolor red
			PressTheAnyKeyToExit
		}
	}
	# Create Shortcut
	if ($Config.CreateDesktopShortcut -eq $True){
		$DesktopPath = [Environment]::GetFolderPath("Desktop")
		$Targetfile = "-ExecutionPolicy Bypass -File `"$WorkingDirectory\$ScriptFileName`""
		$ShortcutFile = "$DesktopPath\萌爸-多开启动器.lnk"
		$WScriptShell = New-Object -ComObject WScript.Shell
		$Shortcut = $WScriptShell.CreateShortcut($ShortcutFile)
		# Use preferred terminal for shortcut
		try { $wt = Get-Command wt.exe -ErrorAction SilentlyContinue } catch { $wt = $null }
		$useWT = ($Script:PreferredTerminal -eq "WT" -and $null -ne $wt)
		if ($useWT) {
			$Shortcut.TargetPath = $wt.Source
			$Shortcut.Arguments = "-w 0 nt powershell -NoProfile $TargetFile"
		}
		else {
			$Shortcut.TargetPath = "powershell.exe"
			$Shortcut.Arguments = $TargetFile
		}
		if ($ShortcutCustomIconPath.length -eq 0){
			$IconPath = "$Script:GamePath\D2R.exe"
			if (-not (Test-Path $IconPath) -and $Script:GamePathCN){ $IconPath = "$Script:GamePathCN\D2R.exe" }
			$Shortcut.IconLocation = $IconPath
		}
		Else {
			$Shortcut.IconLocation = $ShortcutCustomIconPath
		}
		$Shortcut.Save()
	}
	#Check if SetTextv2.exe exists, if not, compile from SetTextv2.bas. SetTextv2.exe is what's used to rename the windows.
	if ((Test-Path -Path ($workingdirectory + '\SetText\SetTextv2.exe')) -ne $True){ #-PathType Leaf check windows renamer is configured.
		Write-Host "`n 首次运行!`n" -foregroundcolor Yellow
		Write-Host " 窗口改名工具 SetTextv2.exe 在 SetText 目录下没有找到，为你现在编译一个."
		if ((Test-Path -Path "C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc.exe") -ne $True){#check that .net4.0 is actually installed or compile will fail.
			Write-Host " .Net v4.0 没有安装. 编译窗口修改工具需要该环境." -foregroundcolor red
			Write-Host " 请手动下载并安装（微软官方链接）:" -foregroundcolor red
			Write-Host " https://dotnet.microsoft.com/en-us/download/dotnet-framework/net40" #actual download link https://dotnet.microsoft.com/en-us/download/dotnet-framework/thank-you/net40-web-installer
			PressTheAnyKeyToExit
		}
		Write-Host " 开始编译 SetTextv2.exe ..."
		& "C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc.exe" -target:winexe -out:"`"$WorkingDirectory\SetText\SetTextv2.exe`"" "`"$WorkingDirectory\SetText\SetTextv2.bas`"" | out-null #/verbose  #actually compile the bastard
		if ((Test-Path -Path ($workingdirectory + '\SetText\SetTextv2.exe')) -ne $True){#if it fails for some reason and settextv2.exe still doesn't exist.
			Write-Host " SetTextv2 编译失败 :/"
			PressTheAnyKeyToExit
		}
		Write-Host " 成功编译 SetTextv2.exe 窗口改名工具 :)" -foregroundcolor green
		Start-Sleep -milliseconds 4000 #a small delay so the first time run outputs can briefly be seen
	}
	#Check Handle64.exe downloaded and placed into correct folder
	$Script:WorkingDirectory = ((Get-ChildItem -Path $PSScriptRoot)[0].fullname).substring(0,((Get-ChildItem -Path $PSScriptRoot)[0].fullname).lastindexof('\'))
	if ((Test-Path -Path ($workingdirectory + '\Handle\Handle64.exe')) -ne $True){ #-PathType Leaf check windows renamer is configured.
		try {
			Write-Host "`n Handle64.exe not in .\Handle\ folder. Downloading now..." -foregroundcolor Yellow
			try {
				New-Item -ItemType Directory -Path ($Script:WorkingDirectory + "\Handle\ExtractTemp\") -ErrorAction stop | Out-Null #create temporary folder to download zip to and extract
			}
			Catch {#if folder already exists for whatever reason.
				Remove-Item -Path ($Script:WorkingDirectory + "\Handle\ExtractTemp\") -Recurse -Force
				New-Item -ItemType Directory -Path ($Script:WorkingDirectory + "\Handle\ExtractTemp\") | Out-Null #create temporary folder to download zip to and extract
			}
			$ZipURL = "https://download.sysinternals.com/files/Handle.zip" #get zip download URL
			$ZipPath = ($WorkingDirectory + "\Handle\ExtractTemp\")
			Invoke-WebRequest -Uri $ZipURL -OutFile ($ZipPath + "\Handle.zip")
			Expand-Archive -Path ($ZipPath + "\Handle.zip") -DestinationPath $ZipPath -Force
			Copy-Item -Path ($ZipPath + "Handle64.exe") -Destination ($Script:WorkingDirectory + "\Handle\")
			Remove-Item -Path ($Script:WorkingDirectory + "\Handle\ExtractTemp\") -Recurse -Force #delete update temporary folder
			Write-Host " Successfully downloaded Handle64.exe :)" -ForeGroundcolor Green
			Start-Sleep -milliseconds 2024
		}
		Catch {
			Write-Host " Handle.zip couldn't be downloaded." -foregroundcolor red
			FormatFunction -Text "It's possible the download link changed. Try checking the Microsoft page or SysInternals.com site for a download link and ensure that handle64.exe is placed in the .\Handle\ folder." -IsError
			Write-Host "`n $X[38;2;69;155;245;4mhttps://learn.microsoft.com/sysinternals/downloads/handle$X[0m"
			Write-Host " $X[38;2;69;155;245;4mhttps://download.sysinternals.com/files/Handle.zip$X[0m`n"
			PressTheAnyKeyToExit
		}
	}
	#Set Region Array
	$Script:ServerOptions = @(
		[pscustomobject]@{Option='0';region='CN';region_server='cn.actual.battlenet.com.cn'} #China
		[pscustomobject]@{Option='1';region='NA';region_server='us.actual.battle.net'} #Americas
		[pscustomobject]@{Option='2';region='EU';region_server='eu.actual.battle.net'} #Europe
		[pscustomobject]@{Option='3';region='KR';region_server='kr.actual.battle.net'} #Asia
	)
}
Function ValidateTokenInput { #For Authentication
	param (
		[bool] $ManuallyEntered,
		[string] $TokenInput,
		[string] $AccountLabel
	)
	do {
		$extractedInfo = $null
		if ($ManuallyEntered){
			$TokenInput = Read-host (" Enter your token URL or enter the token for account '" + $AccountLabel + "'")
		}
		$pattern = "(?<=\?ST=|&ST=|^|http://localhost:0/\?ST=)([^&]+)"
		if ($tokeninput -match $pattern){
			$extractedInfo = ($matches[1]).replace("http://localhost:0/?ST=","")
			return $extractedInfo
		}
		Else {
			Write-Host " Token details are incorrect." -foregroundcolor red
			if (!$ManuallyEntered){
				Write-Host " Please review the setup instructions.`n" -foregroundcolor red
				PressTheAnyKeyToExit
			}
			Else {
				Write-Host " Please enter the full URL." -foregroundcolor red
			}
		}
	}
	until ($null -ne $extractedInfo)
}
Function ImportCSV { #Import Account CSV
	if (-not ($Script:ParamsUsed -eq $True -and $Null -ne $Script:AccountUsername -and $Null -ne $Script:PW)) {#Do this if no parameters are used or parameters that are used don't depend on info in accounts.csv
		do {
			try {
				write-host " Importing accounts.csv..."
				$Script:AccountOptionsCSV = import-csv "$Script:WorkingDirectory\Accounts.csv" #import all accounts from csv
			}
			Catch {
				FormatFunction -Text "`nAccounts.csv does not exist. Make sure you create this and populate with accounts first." -IsError
				PressTheAnyKeyToExit
			}
			if ($Null -ne $Script:AccountOptionsCSV){ #if accounts.csv isn't empty
				#check Accounts.csv has been updated and doesn't contain the example account.
				if ($Script:AccountOptionsCSV -match "yourbnetemailaddress"){
					Write-Host "`n You haven't setup accounts.csv with your accounts." -foregroundcolor red
					Write-Host " Add your account details to the CSV file and run the script again :)`n" -foregroundcolor red
					PressTheAnyKeyToExit
				}
				if ($Null -ne ($AccountOptionsCSV | Where-Object {$_.id -eq ""})){
					if ($Null -ne ($AccountOptionsCSV | Where-Object {$_.id -eq "" -and $_.Acct -eq ""})){
						$Script:AccountOptionsCSV = $Script:AccountOptionsCSV | Where-Object {$_.id -ne "" -or $_.Acct -ne ""} # To account for user error, remove any empty rows from accounts.csv
						$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
					}
					if ($Null -ne ($AccountOptionsCSV | Where-Object {$_.id -eq "" -and $_.Acct -ne ""})){
						formatfunction -IsError -Text "Accounts.csv is misconfigured: One or more of the accounts doesn't have an ID assigned (it was left blank)."
						formatfunction -IsError -Text "Fix this by editing the accounts.csv file and adding the missing ID number. Once done, rerun the script.`n"
						Start-Sleep -milliseconds 1234
						PressTheAnyKeyToExit
					}
				}
				ForEach ($Account in $AccountOptionsCSV){
					if ($Account.accountlabel -eq ""){ # if user doesn't specify a friendly name, use id. Prevents display issues later on.
						$Account.accountlabel = ("Account " + $Account.id)
						$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
					}
				}
				$DuplicateIDs = $AccountOptionsCSV | Where-Object {$_.id -ne ""} | Group-Object -Property ID | Where-Object { $_.Count -gt 1 }
				if ($duplicateIDs.Count -gt 0){
					$duplicateIDs = ($DuplicateIDs.name | out-string).replace("`r`n",", ").trim(", ") #outputs more meaningful error.
					Write-Host "`n Accounts.csv has duplicate IDs: $duplicateIDs" -foregroundcolor red
					FormatFunction -Text "Please adjust Accounts.csv so that the ID numbers against each account are unique.`n" -IsError
					PressTheAnyKeyToExit
				}
				if (-not ($Script:AccountOptionsCSV | Get-Member -Name "Batches" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#For update 1.7.0. If batch column doesn't exist, add it
					# Column does not exist, so add it to the CSV data
					$Script:AccountOptionsCSV | ForEach-Object {
						$_ | Add-Member -NotePropertyName "Batches" -NotePropertyValue $Null
					}
					# Export the updated CSV data back to the file
					$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
				}
				$BatchesInCSV = ($AccountOptionsCSV | group-object batches | where-object {$_.name -ne ""}).count
				if ($BatchesInCSV -ge 1 -or $Null -ne $Batch){
					$Script:EnableBatchFeature = $True
					$Script:BatchOption = "b" #specified here as well as in the ChooseAccounts section so that this works when being passed as a parameter
				}
				if (-not ($Script:AccountOptionsCSV | Get-Member -Name "Token" -MemberType NoteProperty -ErrorAction SilentlyContinue) -or -not ($Script:AccountOptionsCSV | Get-Member -Name "AuthenticationMethod" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#For update 1.10.0. If token columns don't exist, add them. As of 1.12.0 onwards, TokenSecureString is no longer used.
					# Column does not exist, so add it to the CSV data
					if (-not ($Script:AccountOptionsCSV | Get-Member -Name "Token" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
						$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "Token" -NotePropertyValue $Null}
					}
					if (-not ($Script:AccountOptionsCSV | Get-Member -Name "AuthenticationMethod" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
						$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "AuthenticationMethod" -NotePropertyValue "Parameter"}
					}
					# Export the updated CSV data back to the file
					$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
				}
				$OldColumnsToRemove = @("PWIsSecureString","TokenIsSecureString") #Old Account.csv columns that are now unused.
				$ExistingColumns = $Script:AccountOptionsCSV | Select-Object -First 1 | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name
				$ColumnsRemoved = $ExistingColumns | Where-Object { $OldColumnsToRemove -contains $_ }
				$DesiredColumnOrder = @("ID","Acct","AccountLabel","Batches","TimeActive","CustomLaunchArguments","AuthenticationMethod","PW","Token","WindowXCoordinates","WindowYCoordinates","WindowHeight","WindowWidth") # Update with your desired column order
				if ($ColumnsRemoved.Count -gt 0){
					$Script:AccountOptionsCSV = $Script:AccountOptionsCSV | Select-Object -Property $DesiredColumnOrder
					$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation #rewrite to accounts.csv to remove unused columns.
					FormatFunction -Text ("Unused Columns were removed from Accounts.csv: " + ($columnsRemoved -join ", ") + ".`n") -IsWarning
					start-sleep -milliseconds 3000
				}
				else {
					Write-Verbose "No columns were removed as they do not exist in the CSV."
				}
				if (-not ($Script:AccountOptionsCSV | Get-Member -Name "CustomLaunchArguments" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#For update 1.8.0. If CustomLaunchArguments column doesn't exist, add it
					# Column does not exist, so add it to the CSV data
					$Script:AccountOptionsCSV | ForEach-Object {
						$_ | Add-Member -NotePropertyName "CustomLaunchArguments" -NotePropertyValue $Script:OriginalCommandLineArguments
					}
					# Export the updated CSV data back to the file
					$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
					Write-Host " Added CustomLaunchArguments column to accounts.csv.`n" -foregroundcolor green
					Start-Sleep -milliseconds 1200
					PressTheAnyKey
				}
				if (-not ($Script:AccountOptionsCSV | Get-Member -Name "TimeActive" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#For update 1.8.0. If TimeActive column doesn't exist, add it
					# Column does not exist, so add it to the CSV data
					$Script:AccountOptionsCSV | ForEach-Object {
						$_ | Add-Member -NotePropertyName "TimeActive" -NotePropertyValue $Null
					}
					# Export the updated CSV data back to the file
					$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
					Write-Host " Added TimeActive column to accounts.csv." -foregroundcolor Green
					PressTheAnyKey
				}
				#Secure any plain text tokens. Ask for tokens on accounts that don't have any if Config is configured to use Token Authentication.
				$NewCSV = ForEach ($Entry in $Script:AccountOptionsCSV){
					if 	($Entry.AuthenticationMethod -eq ""){
						$Entry.AuthenticationMethod = "Parameter"
						$UpdateAccountsCSV = $True
					}
					if ($Entry.AuthenticationMethod -ne "Token" -and $Entry.AuthenticationMethod -ne "Parameter"){
						Write-Host ("`n Error: AuthenticationMethod in accounts.csv for " + $Entry.AccountLabel + " is invalid.") -Foregroundcolor red
						Write-Host " Open accounts.csv and set this to either 'Parameter' or 'Token'.`n" -Foregroundcolor red
						PressTheAnyKeyToExit
					}
					if ($Entry.Token.length -ge 200){#if nothing needs converting, make sure existing entries still make it into the updated CSV
						$Entry
					}
					#if ($Entry.Token.length -eq 0){#If no token is populated, lets automatically get it so user doesn't have too!
						#Implement in the future if possible. Can't see a way to do this currently without using an external .exe.
					#}
					if ($Entry.Token.length -lt 200 -and $Entry.Token.length -ne 0 -and $Config.ConvertPlainTextSecrets -eq $True){#if accounts.csv has a Token and it's less than 200 chars long, it hasn't been secured yet.
						$ValidatedToken = ValidateTokenInput -TokenInput $Entry.Token
						$Entry.Token = ConvertTo-SecureString -String $ValidatedToken -AsPlainText -Force
						$Entry.Token = $Entry.Token | ConvertFrom-SecureString
						Write-Host (" Secured Token for " + $Entry.AccountLabel) -foregroundcolor green
						Start-Sleep -milliseconds 100
						$Entry
						$TokensUpdated = $true
					}
					if ($Entry.Token.length -eq 0 -and $Entry.AuthenticationMethod -eq "Token"){#if csv has account details but Token field has been left blank
						Write-Host ("`n The account " + $Entry.AccountLabel + " doesn't yet have a Token defined.") -foregroundcolor yellow
						Write-Host " See the readme on Github for how to obtain Auth token.`n" -foregroundcolor yellow
						Write-Host " #3-setup-your-accounts`n" -foregroundcolor cyan
						while ($Entry.Token.length -eq 0){#prevent empty entries as this will cause errors.
							$Entry.Token = ValidateTokenInput -ManuallyEntered $True -AccountLabel $Entry.AccountLabel
						}
						if ($Config.ConvertPlainTextSecrets -eq $True){
							$Entry.Token = ConvertTo-SecureString -String $Entry.Token -AsPlainText -Force
							$Entry.Token = $Entry.Token | ConvertFrom-SecureString
							Write-Host (" Secured Token for " + $Entry.AccountLabel) -foregroundcolor green
							Start-Sleep -milliseconds 100
						}
						$Entry
						$TokensUpdated = $true
					}
					if ($Entry.Token.length -ge 200 -or ($Config.ConvertPlainTextSecrets -eq $False -and $Entry.Token.length -gt 10)){
						$Script:TokensConfigured = $True
					}
				}
				#Check CSV for Plain text Passwords, convert to encryptedstrings and replace values in CSV
				$NewCSV = ForEach ($Entry in $Script:AccountOptionsCSV){
					if ($Entry.PW.length -ge 300 -and $Config.ConvertPlainTextSecrets -eq $True){#if nothing needs converting, make sure existing entries still make it into the updated CSV
						$Entry
					}
					ElseIf ($Entry.PW.length -ge 1 -and $Config.ConvertPlainTextSecrets -eq $False){#if nothing needs converting, make sure existing entries still make it into the updated CSV
						$Entry
					}
					if ($Entry.PW.length -lt 300 -and $Entry.PW.length -ne 0 -and $Config.ConvertPlainTextSecrets -ne $False){#if accounts.csv has a password and it isn't over 300characters, this means it's not converted yet. As such, convert PW to secure string and update CSV.
						$Entry.PW = ConvertTo-SecureString -String $Entry.PW -AsPlainText -Force
						$Entry.PW = $Entry.PW | ConvertFrom-SecureString
						Write-Host (" Secured Password for " + $Entry.AccountLabel) -foregroundcolor green
						Start-Sleep -milliseconds 100
						$Entry
						$PWsUpdated = $true
					}
					if ($Entry.PW.length -eq 0){#if csv has account details but password field has been left blank
						Write-Host ("`n The account " + $Entry.AccountLabel + " doesn't yet have a password defined.`n") -foregroundcolor yellow
						if ($Config.ConvertPlainTextSecrets -eq $True){
							while ($Entry.PW.length -eq 0){#prevent empty entries as this will cause errors.
								$Entry.PW = read-host -AsSecureString " Enter the Battle.net password for"$Entry.AccountLabel
							}
							$Entry.PW = $Entry.PW | ConvertFrom-SecureString
							Write-Host (" Secured Password for " + $Entry.AccountLabel) -foregroundcolor green
						}
						Else { #if passwords aren't to be secured
							while ($Entry.PW.length -eq 0){#prevent empty entries as this will cause errors.
								$Entry.PW = read-host " Enter the Battle.net password for"$Entry.AccountLabel
							}
							Write-Host (" Saved Password for " + $Entry.AccountLabel) -foregroundcolor green
						}
						Start-Sleep -milliseconds 100
						$Entry
						$PWsUpdated = $true
					}
				}
				if ($PWsUpdated -eq $true -or $TokensUpdated -eq $True -or $UpdateAccountsCSV -eq $True){#if CSV needs to be updated
					Try {
						$NewCSV | Export-CSV "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation #update CSV file
						if ($Config.ConvertPlainTextSecrets -eq $True){$SavedOrSecured = "Secured"}Else{$SavedOrSecured = "Saved"}
						if ($TokensUpdated -eq $True){
							Write-Host " Accounts.csv updated: Tokens have been $SavedOrSecured." -foregroundcolor green
						}
						if ($PWsUpdated -eq $True){
							Write-Host " Accounts.csv updated: Passwords have been $SavedOrSecured." -foregroundcolor green
						}
						Start-Sleep -milliseconds 3000
					}
					Catch {
						Write-Host "`n Couldn't update Accounts.csv, probably because the file is open & locked." -foregroundcolor red
						Write-Host " Please close accounts.csv and run the script again!" -foregroundcolor red
						Write-Host "`n If Accounts.csv isn't open, then there must be a permission issue." -foregroundcolor red
						Write-Host " Try moving your install folder to a different location." -foregroundcolor red
						PressTheAnyKeyToExit
					}
				}
				$AccountCSVImportSuccess = $True
			}
			elseif ($Null -eq $Script:AccountUsername){# If script parameters haven't been used, Error out and exit if there's a problem with the csv.
				if ($AccountCSVRecoveryAttempt -lt 1){
					try {
						Write-Host " Issue with accounts.csv. Attempting Autorecovery from backup..." -foregroundcolor red
						Copy-Item -Path $Script:WorkingDirectory\Accounts.backup.csv -Destination $Script:WorkingDirectory\Accounts.csv -ErrorAction stop
						Write-Host " Autorecovery successful!" -foregroundcolor Green
						$AccountCSVRecoveryAttempt ++
						PressTheAnyKey
					}
					Catch {
						$AccountCSVImportSuccess = $False
					}
				}
				Else {
					$AccountCSVRecoveryAttempt = 2
				}
				if ($AccountCSVImportSuccess -eq $False -or $AccountCSVRecoveryAttempt -eq 2){
					Write-Host "`n There's an issue with accounts.csv." -foregroundcolor red
					Write-Host " Please ensure that this is filled out correctly and rerun the script." -foregroundcolor red
					Write-Host " Alternatively, rebuild CSV from scratch or restore from accounts.backup.csv`n" -foregroundcolor red
					PressTheAnyKeyToExit
				}
			}
		} until ($AccountCSVImportSuccess -eq $True)
		$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv"
		([int]$Script:CurrentStats.TimesLaunched) ++
		if ($CurrentStats.TotalGameTime -eq ""){
			$Script:CurrentStats.TotalGameTime = 0 #prevents errors from happening on first time run.
		}
		try {
			$CurrentStats | Export-Csv -Path "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation #update Stats.csv with Total Time played.
		}
		Catch {
			Write-Host "  Couldn't update stats.csv" -foregroundcolor yellow
		}
		#Make Backup of CSV.
		 # Added this in as I had BSOD on my PC and noticed that this caused the files to get corrupted.
		Copy-Item -Path ($Script:WorkingDirectory + "\stats.csv") -Destination ($Script:WorkingDirectory + "\stats.backup.csv")
		Copy-Item -Path ($Script:WorkingDirectory + "\accounts.csv") -Destination ($Script:WorkingDirectory + "\accounts.backup.csv")
	}
	else {
		$Script:ParamLaunchAndAccountNotInAccountsCSV = $True
		write-verbose "Parameters used with account and PW specified. Accounts.csv not imported."
	}
}
Function SetQualityRolls {
	#Set item quality array for randomizing quote colours. A stupid addition to script but meh.
	$Script:QualityArray = @(#quality and chances for things to drop based on 0MF values in D2r (I think?)
		[pscustomobject]@{Type='HighRune';Probability=1}
		[pscustomobject]@{Type='Unique';Probability=50}
		[pscustomobject]@{Type='SetItem';Probability=124}
		[pscustomobject]@{Type='Rare';Probability=200}
		[pscustomobject]@{Type='Magic';Probability=588}
		[pscustomobject]@{Type='Normal';Probability=19036}
	)
	if ($Script:GemActivated -eq $True){#small but noticeable MF boost
		$Script:QualityArray | Where-Object { $_.Type -eq 'Normal' } | ForEach-Object {
			$_.Probability = 16384  # New probability value
		}
	}
	Else {
		$Script:QualityArray | Where-Object { $_.Type -eq 'Normal' } | ForEach-Object {
			$_.Probability = 19036  # Original probability value
		}
	}
	if ($Script:CowKingActivated -eq $True){#big MF boost
		$Script:QualityArray | Where-Object { $_.Type -eq 'Normal' } | ForEach-Object {
			$_.Probability = 2048  # New probability value
			$Script:MOO = "MOO"
		}
	}
	if ($Script:PGemActivated -eq $True){#huuge MF boost
		$Script:QualityArray | Where-Object { $_.Type -eq 'Normal' } | ForEach-Object {
			$_.Probability = 192  # New probability value
		}
	}
	$QualityHash = @{};
	ForEach ($Object in $Script:QualityArray | select-object type,probability){#convert PSOobjects to hashtable for enumerator
		$QualityHash.add($Object.type,$Object.probability) #add each PSObject to hash
	}
	$Script:ItemLookup = ForEach ($Entry in $QualityHash.GetEnumerator()){
		[System.Linq.Enumerable]::Repeat($Entry.Key, $Entry.Value) #This creates a hash table with 19036 normal items, 588 magic items, 200 rare items etc etc. Used later as a list to randomly pick from.
	}
}
Function CowKingKilled {
	Write-Host "`n                          You Killed the Cow King!" -foregroundcolor green
	Write-Host "                                $X[38;2;165;146;99;22mMoo.$X[0m"
	Write-Host "                                    $X[38;2;165;146;99;22mMoooooooo!$X[0m"
	$voice = New-Object -ComObject Sapi.spvoice
	$voice.rate = -4 #How quickly the voice message should be
	$voice.volume = $Config.DCloneAlarmVolume
	[void](Set-PreferredVoice -SpVoice $voice -Preference $Script:Config.DCloneAlarmVoice)
	Invoke-LocalizedSpeech -SpVoice $voice -MessageEnUS "MooMoo-moo moo-moo, moo-moo, moo moo."
	$CowLogo = @"
 -)[<-
   +[[[*
     *)}]>>=-----                   +:                   =-
      :=]#}[[[]))])>-               [>-     :------     =)*:
     :<]<<<][[]))][<*=              <]<>]]<>[%%%##}<><<<]]*
   :=*)))**)))[[]]>:                 -<)[}#%%%%%%%%%%%%[<
     -*><)]))))<)][[*:                    -[#%%%%%%%%%}-
       =*>><<<)<->[}}[*:                 *}#<>%%%)=)%%%}*
        :+>><)]])]]<[##[*:              *@@%%#[[[[}%%%%%}
          :-<<)))<)*  *#}[):            -}%@#[]]]]#%%%%%%>
              -=+><-   :>[#})+          :}%%#[[[]]###%%%%%>
                          -]#}}-:      -]%#}}[}}[[[[}#%%%%):
                            :>}})-    +#%%%#}}[][[)[}#%%%%#+
                               =##%%<=}}}}}}}}}}[[])}#%%%%%#+
                            :>[[##}}[[}[[[[[][]]]))))]]]]}#%%-
                            :]##}]<)][}[}#}}}}}[[[]])]][[}##%}:
                             =<}]>****+<[[}}}}[[[]]]]]][}}}###<
                                       :#%#[[[]]])))))][%%}[}}[-
                                       :#%###}[]]]))))][#%}]][]-
                                       :#%##}}}}[[[]]][[#}))<>-:
                                       :#####}}}}}[[[[]<>)}[:
                                         [#}}}}}}}}[}}##}%%+
                                         :#}}}}}}}}}[[[}}}}>-
                                         >##}}}}}}}}[[[[[}#[[<
                                        =}}}##}}}[[[[[[])]##<+
                                        >#}}#}}[[[[][]]]))#%*
                                       :]#}}###}[[[[[][[)]#[:
                                        *#}}#}}}}}}}[[[}[}#[:
          (__)                          *###}##}}}}}}}}}}#%):
          (oo)                          :[}}}}<]][[[}###}##+
   /-------\/                            :}}}}>    :)###}}#]-
  / |     ||                              +}}}>      +}%#}#%>
  * ||----||                              >}}[:        -=]}#<
    ||    ||                             -####*          )}]<:
                                        +#%%%}           -[]]>
                                        :-*+-:           -][])
                                                         =[]))+
                                                         =}##}):
"@
	Write-Host "  $X[38;2;255;165;0;22m$CowLogo$X[0m"
	$Script:CowKingActivated = $True
	([int]$Script:CurrentStats.CowKingKilled) ++
	SetQualityRolls
	start-sleep -milliseconds 4550
	try {
		$CurrentStats | Export-Csv -Path "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation #update Stats.csv with Total Time played and cow stats.
	}
	Catch {
		Write-Host "  Couldn't update stats.csv" -foregroundcolor yellow
	}
}
Function HighRune {
	process { Write-Host "  $X[38;2;255;165;000;48;2;1;1;1;4m$_$X[0m"}
}
Function Unique {
	process { Write-Host "  $X[38;2;165;146;99;48;2;1;1;1;4m$_$X[0m"}
}
Function SetItem {
	process { Write-Host "  $X[38;2;0;225;0;48;2;1;1;1;4m$_$X[0m"}
}
Function Rare {
	process { Write-Host "  $X[38;2;255;255;0;48;2;1;1;1;4m$_$X[0m"}
}
Function Magic {#ANSI text colour formatting for "magic" quotes. The variable $X (for the escape character) is defined earlier in the script.
	process { Write-Host "  $X[38;2;65;105;225;48;2;1;1;1;4m$_$X[0m" }
}
Function Normal {
	process { Write-Host "  $X[38;2;255;255;255;48;2;1;1;1;4m$_$X[0m"}
}
Function QuoteRoll {#stupid thing to draw a random quote but also draw a random quality.
	$Quality = get-random $Script:ItemLookup #pick a random entry from ItemLookup hashtable.
	Write-Host
	$LeQuote = (Get-Random -inputobject $Script:quotelist) #pick a random quote.
	$ConsoleWidth = $Host.UI.RawUI.BufferSize.Width
	$DesiredIndent = 2  # indent spaces
	$ChunkSize = $ConsoleWidth - $DesiredIndent
	[RegEx]::Matches($LeQuote, ".{$ChunkSize}|.+").Groups.Value | ForEach-Object {
		Write-Output $_ | &$Quality #write the quote and write it in the quality colour
	}
	if ($LeQuote -match "Moo" -and $Quality -eq "Unique"){
		CowKingKilled
	}
	$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv"
	if ($Quality -eq "HighRune"){([int]$Script:CurrentStats.HighRunesFound) ++}
	if ($Quality -eq "Unique"){([int]$Script:CurrentStats.UniquesFound) ++}
	if ($Quality -eq "SetItem"){([int]$Script:CurrentStats.SetItemsFound) ++}
	if ($Quality -eq "Rare"){([int]$Script:CurrentStats.RaresFound) ++}
	if ($Quality -eq "Magic"){([int]$Script:CurrentStats.MagicItemsFound) ++}
	if ($Quality -eq "Normal"){([int]$Script:CurrentStats.NormalItemsFound) ++}
	try {
		$CurrentStats | Export-Csv -Path "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation #update Stats.csv
	}
	Catch {
		Write-Host "  Couldn't update stats.csv" -foregroundcolor yellow
		Start-Sleep -Milliseconds 256
	}
}
Function Inventory {#Info screen
	Clear-Host
	Write-Host "`n          Stay a while and listen! 这里是 D2RLoader 的信息页.`n" -foregroundcolor yellow
	Write-Host " 游戏时间的统计，基于本脚本运行的时间"
	Write-Host "   (假设你的 D2R 总是通过本工具启动，且本窗口始终挂在后台运行)`n"
	$QualityArraySum = 0
	$Script:QualityArray | ForEach-Object {
		$QualityArraySum += $_.Probability
	}
	$NormalProbability = ($QualityArray | where-object {$_.type -eq "Normal"} | Select-Object Probability).probability
	$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv"
	$Line1 =   "                    ----------------------------------"
	$Line2 =  ("                   |  $X[38;2;255;255;255;22m游戏时间 (小时):$X[0m " +  ((($time =([TimeSpan]::Parse($CurrentStats.TotalGameTime))).hours + ($time.days * 24)).tostring() + ":" + ("{0:D2}" -f $time.minutes)))
	$Line3 =  ("                   |  $X[38;2;255;255;255;22m当前会话 (小时):$X[0m" + ((($time =([TimeSpan]::Parse($Script:SessionTimer))).hours + ($time.days * 24)).tostring() + ":" + ("{0:D2}" -f $time.minutes)))
	$Line4 =  ("                   |  $X[38;2;255;255;255;22m脚本启动计数:$X[0m " + $CurrentStats.TimesLaunched)
	$Line5 =   "                    ----------------------------------"
	$Line6 =  ("                   |  $X[38;2;255;165;000;22m高级符文$X[0m Found: " + $(if ($CurrentStats.HighRunesFound -eq ""){"0"} else {$CurrentStats.HighRunesFound}))
	$Line7 =  ("                   |  $X[38;2;165;146;99;22m暗金$X[0m Quotes Found: " + $(if ($CurrentStats.UniquesFound -eq ""){"0"} else {$CurrentStats.UniquesFound}))
	$Line8 =  ("                   |  $X[38;2;0;225;0;22m绿装$X[0m Quotes Found: " + $(if ($CurrentStats.SetItemsFound -eq ""){"0"} else {$CurrentStats.SetItemsFound}))
	$Line9 =  ("                   |  $X[38;2;255;255;0;22m黄装$X[0m Quotes Found: " + $(if ($CurrentStats.RaresFound -eq ""){"0"} else {$CurrentStats.RaresFound}))
	$Line10 = ("                   |  $X[38;2;65;105;225;22m蓝装$X[0m Quotes Found: " + $(if ($CurrentStats.MagicItemsFound -eq ""){"0"} else {$CurrentStats.MagicItemsFound}))
	$Line11 = ("                   |  $X[38;2;255;255;255;22m普通$X[0m Quotes Found: " + $(if ($CurrentStats.NormalItemsFound -eq ""){"0"} else {$CurrentStats.NormalItemsFound}))
	$Line12 =  "                    ----------------------------------"
	$Line13 = ("                   |  $X[38;2;165;146;99;22m牛王被杀:$X[0m " + $(if ($CurrentStats.CowKingKilled -eq ""){"0"} else {$CurrentStats.CowKingKilled}))
	$Line14 = ("                   |  $X[38;2;255;0;255;22m大厅宝石激活:$X[0m  " + $(if ($CurrentStats.Gems -eq ""){"0"} else {$CurrentStats.Gems}))
	$Line15 = ("                   |  $X[38;2;255;0;255;22m完美宝石激活:$X[0m " + $(if ($CurrentStats.PerfectGems -eq ""){"0"} else {$CurrentStats.PerfectGems}))
	$Line16 =  "                    ----------------------------------"
	$Lines = @($Line1,$Line2,$Line3,$Line4,$Line5,$Line6,$Line7,$Line8,$Line9,$Line10,$Line11,$Line12,$Line13,$Line14,$Line15,$Line16)
	# 稳妥布局：
	# - 横线：左侧多 1 空格缩进，右侧比内容短 1 列，避免溢出
	# - 内容行：值右对齐，'|' 前仅留 1 空格
	$LongestContent = 0
	ForEach ($Line in $Lines){ if ($Line -notmatch "-" ){ $len = GetDisplayWidth $Line; if ($len -gt $LongestContent){ $LongestContent = $len } } }
	ForEach ($Line in $Lines){
		$curr = GetDisplayWidth $Line
		if ($Line -notmatch "-"){
			$lineOut = $Line
			$left = $lineOut; $right = ""
			$m = [regex]::Match($lineOut, "^(.*?)(\d+:[0-9]{2})\s*$")
			if ($m.Success){ $left = $m.Groups[1].Value; $right = $m.Groups[2].Value }
			else {
				$m2 = [regex]::Match($lineOut, "^(.*?[:：]\s*)(\d+)\s*$")
				if ($m2.Success){ $left = $m2.Groups[1].Value; $right = $m2.Groups[2].Value }
				else { $anchorNo = ($X + "[0m"); $idx = $lineOut.LastIndexOf($anchorNo); if ($idx -ge 0){ $left = $lineOut.Substring(0, $idx + $anchorNo.Length); $right = $lineOut.Substring($idx + $anchorNo.Length).TrimStart() } }
			}
			$leftW = GetDisplayWidth $left; $rightW = GetDisplayWidth $right
			$spaces = [Math]::Max(0, ($LongestContent - $leftW - $rightW))
			$aligned = ($left + (" " * $spaces) + $right)
			$pad = [Math]::Max(0, ($LongestContent + 1) - (GetDisplayWidth $aligned))
			Write-Host ($aligned + (" " * $pad)) -NoNewline
			Write-Host " |" -NoNewline
			Write-Host
		}
		else {
			# 安全重建横线：左缩进取自该行，右侧长度设为 (LongestContent + 1)
			$leftIndentDash = ($Line -replace "-.*$", "")
			$dashGoal = [Math]::Max(0, ($LongestContent + 1))
			$indentW = GetDisplayWidth $leftIndentDash
			$dashLen = [Math]::Max(1, $dashGoal - $indentW)
			Write-Host ($leftIndentDash + ("-" * $dashLen))
		}
	}
	Write-Host ("`n                     获得 $X[38;2;65;105;225;22m魔法$X[0m 箴言 几率提升: " + [math]::Round((($QualityArraySum - $NormalProbability + 1) * (1/$QualityArraySum) * 100),2) + "%" )
	$__printedVer = $false
	if ($Script:GamePath -and (Test-Path "$Script:GamePath\D2R.exe")) {
		$__verGlobal = (Get-Command "$Script:GamePath\D2R.exe").FileVersionInfo.FileVersion
		Write-Host ("`n  $X[4mD2R游戏版本(国际服):$X[0m    " + $__verGlobal)
		$__printedVer = $true
	}
	if ($Script:GamePathCN -and (Test-Path "$Script:GamePathCN\D2R.exe")) {
		$__verCN = (Get-Command "$Script:GamePathCN\D2R.exe").FileVersionInfo.FileVersion
		$__prefix = if ($__printedVer) { "  " } else { "`n  " }
		Write-Host ($__prefix + "$X[4mD2R游戏版本(国服):$X[0m      " + $__verCN)
		$__printedVer = $true
	}
	Write-Host "  $X[4m脚本路径:$X[0m " -nonewline
	Write-Host ("`"$Script:WorkingDirectory`"" -replace "((.{1,52})(?:\\|\s|$)|(.{1,53}))", "`n                        `$1").trim() #add two spaces before any line breaks for indenting. Add line break for paths that are longer than 53 characters.
	Write-Host "  $X[4m脚本版本:$X[0m v$CurrentVersion"
	if ($null -eq $Script:LatestVersionCheck -or $Script:LatestVersionCheck.tostring() -lt (Get-Date).addhours(-2).ToString('yyyy.MM.dd HH:mm:ss')){ #check for updates. Don't check if this has been checked in the couple of hours.
		try {
			$Releases = Invoke-RestMethod -Uri "https://api.github.com/repos/shupershuff/Diablo2RLoader/releases"
			$ReleaseInfo = ($Releases | Sort-Object id -desc)[0] #find release with the highest ID.
			$Script:LatestVersionCheck = (get-date).tostring('yyyy.MM.dd HH:mm:ss')
			$Script:LatestVersion = [version[]]$ReleaseInfo.Name.Trim('v')
		}
		Catch {
			Write-Output "  无法检查更新 :(" | Red
		}
	}
	if ($Null -ne $Script:LatestVersion -and $Script:LatestVersion -gt $Script:CurrentVersion){
		Write-Host "`n  $X[4m最新脚本版本:$X[0m v$LatestVersion" -foregroundcolor yellow
	}
	Write-Host "`n  $X[38;2;0;225;0;22m考虑捐赠作为感谢的一种方式:$X[0m"
	Write-Host "    - $X[38;2;69;155;245;4mhttps://www.buymeacoffee.com/shupershuff$X[0m （shupershuff 原作者）"
	Write-Host "    - $X[38;2;69;155;245;4mhttps://afdian.com/a/knightli$X[0m （萌爸 汉化+国服扩展）"
	if ($Script:NotificationsAvailable -eq $True){
		Write-Host "  "
		Write-Host "  -------------------------------------------------------------------------"
		Write-Host "  $X[38;2;255;165;000;48;2;1;1;1;4m提醒:$X[0m" -nonewline
		Notifications -check $False
		$Script:NotificationHasBeenChecked = $True
		Write-Host "  -------------------------------------------------------------------------"
	}
	Write-Host
	PressTheAnyKey
}
Function LoadWindowClass { #Used to get window locations and place them in the same screen locations at launch. Code courtesy of Sir-Wilhelm and Microsoft.
	try {
		[void][Window]
	}
	catch {
		Add-Type @"
		using System;
		using System.Runtime.InteropServices;
		public class Window {
			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); //get window coordinates
			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public extern static bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw); //set window coordinates
			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool SetForegroundWindow(IntPtr hWnd); //Bring window to front
			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool ShowWindow(IntPtr handle, int state); //used in this script to restore minimized window (state 9)
			
			// Add SetWindowPos
			[DllImport("user32.dll", SetLastError = true)]
			public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
		}
		public struct RECT {
			public int Left;        // x position of upper-left corner
			public int Top;         // y position of upper-left corner
			public int Right;       // x position of lower-right corner
			public int Bottom;      // y position of lower-right corner
		}
"@	}
}
Function SaveWindowLocations {# Get Window Location coordinates and save to Accounts.csv
	LoadWindowClass
	FormatFunction -indents 2 -text "可按账号记忆窗口位置，下次打开时恢复 (前提是窗口模式)"
	CheckActiveAccounts
	#If Feature is enabled, add 'WindowXCoordinates' and 'WindowYCoordinates' columns to accounts.csv with empty values.
	if (-not ($Script:AccountOptionsCSV | Get-Member -Name "WindowXCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue) -or -not ($Script:AccountOptionsCSV | Get-Member -Name "WindowYCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue) -or -not ($Script:AccountOptionsCSV | Get-Member -Name "WindowWidth" -MemberType NoteProperty -ErrorAction SilentlyContinue) -or -not ($Script:AccountOptionsCSV | Get-Member -Name "WindowHeight" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
		# Column does not exist, so add it to the CSV data
		if (-not ($Script:AccountOptionsCSV | Get-Member -Name "WindowXCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
			$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "WindowXCoordinates" -NotePropertyValue $Null}
		}
		if (-not ($Script:AccountOptionsCSV | Get-Member -Name "WindowYCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
			$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "WindowYCoordinates" -NotePropertyValue $Null}
		}
		if (-not ($Script:AccountOptionsCSV | Get-Member -Name "WindowHeight" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
			$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "WindowHeight" -NotePropertyValue $Null}
		}
		if (-not ($Script:AccountOptionsCSV | Get-Member -Name "WindowWidth" -MemberType NoteProperty -ErrorAction SilentlyContinue)){
			$Script:AccountOptionsCSV | ForEach-Object {$_ | Add-Member -NotePropertyName "WindowWidth" -NotePropertyValue $Null}
		}
		# Export the updated CSV data back to the file
		$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
	}
	if ($null -eq $Script:ActiveAccountsList){
		FormatFunction -text "`n你还尚未启动任何账号`n要保存窗口位置, 你需要先启动一个或多个实例.`n" -indents 2 -IsError
		PressTheAnyKey
		Return $False
	}
	$NewCSV = ForEach ($Account in $Script:AccountOptionsCSV){
		if ($account.id -in $Script:ActiveAccountsList.id){
			$process = Get-Process -Id ($Script:ActiveAccountsList | where-object {$_.id -eq $account.id}).ProcessID
			$handle = $process.MainWindowHandle
			Write-Verbose "$($process.ProcessName) `(Id=$($process.ID), Handle=$handle`, Path=$($process.Path))"
			$rectangle = New-Object RECT
			[Window]::GetWindowRect($handle, [ref]$rectangle) | Out-Null
			FormatFunction -indents 2 -text "`n保存窗口位置。账号：$($account.id) ($($account.AccountLabel))" -IsSuccess
			Write-Host "     X Position = $($rectangle.Left)" -Foregroundcolor Green
			Write-Host "     Y Position = $($rectangle.Top)" -Foregroundcolor Green
			write-Host "     Width = $($rectangle.Right - $rectangle.Left)" -Foregroundcolor Green
			write-Host "     Height = $($rectangle.Bottom - $rectangle.Top)" -Foregroundcolor Green
			$Account.WindowXCoordinates = $rectangle.Left
			$Account.WindowYCoordinates = $rectangle.Top
			$Account.WindowWidth = $rectangle.Right - $rectangle.Left
			$Account.WindowHeight = $rectangle.Bottom - $rectangle.Top
			$Account
		}
		Else {#Leave as is.
			$Account
			write-Verbose "账号 $($account.id) ($($account.AccountLabel)) 没有运行."
		}
	}
	$NewCSV | Export-CSV "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation
	Write-Host "`n   更新 CSV 中的窗口位置." -foregroundcolor green
	start-sleep -milliseconds 2500
}
Function SetWindowLocations {# Move windows to preferred location/layout
	param(
		[int]$Id,
		[int]$X,
		[int]$Y,
		[int]$Width,
		[int]$Height
	)
	LoadWindowClass
	$handle = (Get-Process -Id $Id).MainWindowHandle
	# Constants for SetWindowPos
	$HWND_TOPMOST = [IntPtr]::Zero # Change this to [IntPtr]::Zero to avoid topmost if you don't want it to be
	#$SWP_NOMOVE = 0x0002
	#$SWP_NOSIZE = 0x0001
	$SWP_SHOWWINDOW = 0x0040
	$SWP_NOREDRAW = 0x0008
	# Restore the window (if minimized)
	[Window]::ShowWindow($handle, 9)
	Start-Sleep -Milliseconds 10
	# Move the window and set its position
	[Window]::SetWindowPos($handle, $HWND_TOPMOST, $X, $Y, $Width, $Height, $SWP_SHOWWINDOW -bor $SWP_NOREDRAW)
	Start-Sleep -Milliseconds 10
	# Optionally, bring it to the foreground
	[Window]::SetForegroundWindow($handle)
}
Function Options {
	ImportXML
	Clear-Host
	Write-Host "`n 这个屏幕允许你改变脚本配置选项."
	Write-Host " 注意你也可以在 config.xml 中改变这些设置 (和更多)."
	Write-Host " 你可以改变/切换下面的选项:`n"
	$XML = Get-Content "$Script:WorkingDirectory\Config.xml"
  if ($Script:Config.DefaultRegion -eq 0){
		$CurrentDefaultRegion = "CN"
	}
	Elseif ($Script:Config.DefaultRegion -eq 1){
		$CurrentDefaultRegion = "NA"
	}
	ElseIf ($Script:Config.DefaultRegion -eq 2){
		$CurrentDefaultRegion = "EU"
	}
	ElseIf ($Script:Config.DefaultRegion -eq 3){
		$CurrentDefaultRegion = "Asia"
	}
	ElseIf ($Script:Config.DefaultRegion -eq ""){
		$CurrentDefaultRegion = "None Specified"
	}
	Write-Host "  $X[38;2;255;165;000;22m1$X[0m - $X[4mDefaultRegion$X[0m (Currently $X[38;2;255;165;000;22m$CurrentDefaultRegion$X[0m)"
	Write-Host "`n  $X[38;2;255;165;000;22m2$X[0m - $X[4mSettingSwitcherEnabled$X[0m (Currently $X[38;2;255;165;000;22m$(if($Script:Config.SettingSwitcherEnabled -eq 'True'){'Enabled'}else{'Disabled'})$X[0m)"
	Write-Host "  $X[38;2;255;165;000;22m3$X[0m - $X[4mManualSettingSwitcherEnabled$X[0m (Currently $X[38;2;255;165;000;22m$(if($Script:Config.ManualSettingSwitcherEnabled -eq 'True'){'Enabled'}else{'Disabled'})$X[0m)"
	Write-Host "  $X[38;2;255;165;000;22m4$X[0m - $X[4mRememberWindowLocations$X[0m (Currently $X[38;2;255;165;000;22m$(if($Script:Config.RememberWindowLocations -eq 'True'){'Enabled'}else{'Disabled'})$X[0m)"
	Write-Host "`n  $X[38;2;255;165;000;22m5$X[0m - $X[4mDCloneTrackerSource$X[0m (Currently $X[38;2;255;165;000;22m$($Script:Config.DCloneTrackerSource)$X[0m)"
	FormatFunction -indents 1 -SubsequentLineIndents 4 -text ("$X[38;2;255;166;000;22m6$X[0m - $X[4mDCloneAlarmList$X[0m (Currently $X[38;2;255;165;000;22m" + $(if ($Script:Config.DCloneAlarmList -eq ""){"Alarms are disabled"}Else{$Script:Config.DCloneAlarmList}) + "$X[0m)")
	if ($Script:Config.DCloneAlarmList -ne ""){
		Write-Host "  $X[38;2;255;165;000;22m7$X[0m - $X[4mDCloneAlarmLevel$X[0m (Currently $X[38;2;255;165;000;22m$($Script:Config.DCloneAlarmLevel)$X[0m)"
		Write-Host "  $X[38;2;255;165;000;22m8$X[0m - $X[4mDCloneAlarmVoice$X[0m (Currently $X[38;2;255;165;000;22m$($Script:Config.DCloneAlarmVoice)$X[0m)"
		Write-Host "  $X[38;2;255;165;000;22m9$X[0m - $X[4mDCloneAlarmVolume$X[0m (Currently $X[38;2;255;165;000;22m$($Script:Config.DCloneAlarmVolume)$X[0m)"
	}
	if ($Script:TokensConfigured -eq $True){
		foreach ($row in $Script:AccountOptionsCSV){
			if ($row.AuthenticationMethod -eq "Parameter"){$ParametersUsed = $True}
		}
		if ($ParametersUsed -eq $True){
			Write-Host "`n  $X[38;2;255;165;000;22mt$X[0m - 临时强制令牌认证 (仅适用于配置的账号)."
			if ($Script:ForceAuthToken -eq $True){
				Write-Host "      $X[4mForceAuthToken$X[0m (Currently $X[38;2;5;250;5;22mEnabled$X[0m)."
			}
			else {
				Write-Host "      $X[4mForceAuthToken$X[0m (Currently $X[38;2;255;165;000;22mDisabled$X[0m)."
			}
			$AllowTokenConfigMenuOption = $True
		}
	}
	Write-Host "`n 输入以上选项之一来改变设置."
	Write-Host " 否则, 按任何其他键返回主菜单... " -nonewline
	$Option = readkey
	Write-Host;Write-Host
	Function OptionSubMenu {
		param (
			[String]$Description,
			[hashtable]$OptionsList,
			[String]$OptionsText,
			[String]$Current,
			[String]$ConfigName,
			[switch]$OptionInteger
		)
		$XML = Get-Content "$Script:WorkingDirectory\Config.xml" -Raw
		FormatFunction -indents 1 -text "改变设置 for $X[4m$($ConfigName)$X[0m (当前 $X[38;2;255;165;000;22m$($Current)$X[0m).`n"
		FormatFunction -text $Description -indents 1
		Write-Host;Write-Host $OptionsText
		do {
			if ($OptionInteger -eq $True){
				Write-Host "   输入一个介于 $X[38;2;255;165;000;22m1$X[0m 和 $X[38;2;255;165;000;22m99$X[0m 之间的数字或 '$X[38;2;255;165;000;22mc$X[0m' 取消: " -nonewline
				$AcceptableOptions = 1..99 # Allow user to enter 1 to 99
				$NewOptionValue = (ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 27 -TwoDigitAcctSelection $True).tostring()
				$NewValue = $NewOptionValue
			}
			Else {
				Write-Host "   Enter " -nonewline;CommaSeparatedList -NoOr ($OptionsList.keys | sort-object); Write-Host " or '$X[38;2;255;165;000;22mc$X[0m' to cancel: " -nonewline
				$AcceptableOptions = $OptionsList.keys
				$NewOptionValue = (ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 27).tostring()
				$NewValue = $($OptionsList[$NewOptionValue])
			}
			if ($NewOptionValue -notin $AcceptableOptions + "c" + "Esc"){
				Write-Host "   无效输入. 请输入以上选项之一.`n" -foregroundcolor red
			}
		} until ($NewOptionValue -in $AcceptableOptions + "c" + "Esc")
		if ($NewOptionValue -in $AcceptableOptions){
			if ($NewOptionValue -ne "s" -and $NewOptionValue -ne "r" -and $NewOptionValue -ne "a"){
				try {
					$Pattern = "(<$ConfigName>)([^<]*)(</$ConfigName>)"
					$ReplaceString = '{0}{1}{2}' -f '${1}', $NewValue, '${3}'
					$NewXML = ([regex]::Replace($Xml, $Pattern, $ReplaceString)).trim()
					$NewXML | Set-Content -Path "$Script:WorkingDirectory\Config.xml"
					return $True
				}
				Catch {
					Write-Host "`n  无法更新配置 :(" -foregroundcolor red
					start-sleep -milliseconds 2500
					return $False
				}
			}
			ElseIf ($NewOptionValue -eq "s"){
				$PositionsRecorded = SaveWindowLocations
				if ($PositionsRecorded -ne $False){#redundant?
					return $False
				}
			}
			ElseIf ($NewOptionValue -eq "r"){
				LoadWindowClass
				CheckActiveAccounts
				if ($null -eq $Script:ActiveAccountsList){
					FormatFunction -text "`n没有打开的游戏.`n要重置窗口位置, 你需要先启动一个或多个实例.`n" -indents 2 -IsWarning
					PressTheAnyKey
					Return $False
				}
				ForEach ($Account in $Script:AccountOptionsCSV){
					if ($account.id -in $Script:ActiveAccountsList.id){
						if ($account.WindowXCoordinates -ne "" -and $account.WindowYCoordinates -ne ""){
							SetWindowLocations -X $Account.WindowXCoordinates -Y $Account.WindowYCoordinates -Width $Account.WindowWidth -height $Account.WindowHeight -Id ($Script:ActiveAccountsList | where-object {$_.id -eq $account.id}).ProcessID | out-null
						}
					}
				}
				FormatFunction -indents 2 -text "移动游戏窗口回到他们保存的屏幕坐标和重置窗口大小.`n" -IsSuccess
				start-sleep -milliseconds 1500
				Return $False
			}
			ElseIf ($NewOptionValue -eq "a"){
				LoadWindowClass
				CheckActiveAccounts
				if ($null -eq $Script:ActiveAccountsList){
					FormatFunction -text "`n没有打开的游戏.`n要设置窗口位置到替代布局, 你需要先启动一个或多个实例.`n" -indents 2 -IsWarning
					PressTheAnyKey
					Return $False
				}
				ForEach ($Account in $Script:AltWindowLayoutCoords){
					if ($account.id -in $Script:ActiveAccountsList.id){
						if ($account.WindowXCoordinates -ne "" -and $account.WindowYCoordinates -ne ""){
							SetWindowLocations -X $Account.WindowXCoordinates -Y $Account.WindowYCoordinates -Width $Account.WindowWidth -height $Account.WindowHeight -Id ($Script:ActiveAccountsList | where-object {$_.id -eq $account.id}).ProcessID | out-null
						}
					}
				}
				FormatFunction -indents 2 -text "移动游戏窗口到替代坐标和窗口大小.`n" -IsSuccess
				start-sleep -milliseconds 1500
				Return $False
			}
		}
		else {
			Return $False
		}
	}
	if ($Option -eq "1"){ #DefaultRegion
		$Options = @{
			"0" = 0
			"1" = 1
			"2" = 2
			"3" = 3
			"4" = ""
		}
		$XMLChanged = OptionSubMenu -ConfigName "DefaultRegion" -OptionsList $Options -Current $CurrentDefaultRegion `
		-Description "这个选项用于让你可以按回车键而不是手动输入区域在区域选择屏幕." `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m0$X[0m' → CN (国服)`n    选择 '$X[38;2;255;165;000;22m1$X[0m' → NA (美服)`n    选择 '$X[38;2;255;165;000;22m2$X[0m' → EU (欧服)`n    选择 '$X[38;2;255;165;000;22m3$X[0m' → KR (亚服)`n    选择 '$X[38;2;255;165;000;22m4$X[0m' → 无`n"
	}
	ElseIf ($Option -eq "2"){ #SettingSwitcherEnabled
		If ($Script:Config.SettingSwitcherEnabled -eq "False"){
			$Options = @{"1" = "True"}
			$OptionsSubText = "enable"
			$CurrentState = "Disabled"
		}
		Else {
			$Options = @{"1" = "False"}
			$OptionsSubText = "disable"
			$CurrentState = "Enabled"
		}
		$XMLChanged = OptionSubMenu -ConfigName "SettingSwitcherEnabled" -OptionsList $Options -Current $CurrentState `
		-Description "这个选项用于让你可以基于你正在启动的账号切换使用不同的配置.`n一个非常酷的功能!`n具体请看功能说明." `
		-OptionsText "    Choose '$X[38;2;255;165;000;22m1$X[0m' to $OptionsSubText`n"
	}
	ElseIf ($Option -eq "3"){ #ManualSettingSwitcherEnabled
		If ($Script:Config.ManualSettingSwitcherEnabled -eq "False"){
			$Options = @{"1" = "True"}
			$OptionsSubText = "enable"
			$CurrentState = "Disabled"
		}
		Else {
			$Options = @{"1" = "False"}
			$OptionsSubText = "disable"
			$Script:AskForSettings = $False
			$CurrentState = "Enabled"
		}
		$XMLChanged = OptionSubMenu -ConfigName "ManualSettingSwitcherEnabled" -OptionsList $Options -Current $CurrentState `
		-Description "这个选项用于让你可以手动选择游戏应该使用哪个配置文件来启动另一个游戏实例.`n例如如果你想要选择启动时使用  Awesome 或 Poo 图形设置 .`n具体请看功能说明." `
		-OptionsText "    Choose '$X[38;2;255;165;000;22m1$X[0m' to $OptionsSubText`n"
	}
	ElseIf ($Option -eq "4"){ #RememberWindowLocations
		If ($Script:Config.RememberWindowLocations -eq "False"){
			$Options = @{"1" = "True"}
			$OptionsSubText = "enable"
			$DescriptionSubText = "`n一旦激活, 返回这个菜单并选择 '$X[38;2;255;165;000;22ms$X[0m' 选项来保存任何打开的游戏实例的坐标."
			$CurrentState = "Disabled"
		}
		Else {
			$Options = @{"1" = "False";"S" = "PlaceholderValue Only :)"} # SaveWindowLocations function used if user chooses "S"
			$OptionsSubText = "disable"
			$OptionsSubTextAgain = "    选择 '$X[38;2;255;165;000;22ms$X[0m' 来保存当前窗口位置和大小.`n"
			if ($Script:AccountOptionsCSV | Get-Member -Name "WindowXCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue){
				$OptionsSubTextAgain += "    选择 '$X[38;2;255;165;000;22mr$X[0m' 来重置窗口位置和大小.`n"
				$Options += @{"R" = "PlaceholderValue Only :)"}
				if (Test-Path -Path "$Script:WorkingDirectory\AltLayout.csv"){
					$Script:AltWindowLayoutCoords = import-csv "$Script:WorkingDirectory\AltLayout.csv"
					$OptionsSubTextAgain += "    选择 '$X[38;2;255;165;000;22ma$X[0m' 来设置窗口位置到替代布局.`n"
					$Options += @{"A" = "PlaceholderValue Only :)"}
				}
			}
			$DescriptionSubText = "`n选择 '$X[38;2;255;165;000;22ms$X[0m' 选项将保存任何打开的游戏实例的坐标 (和窗口大小).`n选择 '$X[38;2;255;165;000;22mr$X[0m' 选项将移动你的窗口回到他们的默认位置."
			$CurrentState = "Enabled"
		}
		$XMLChanged = OptionSubMenu -ConfigName "RememberWindowLocations" -OptionsList $Options -Current $CurrentState `
		-Description "对于那些已经配置游戏以在窗口模式下启动, 这个设置用于让脚本在启动时移动窗口位置, 所以你永远不需要在启动账户时重新排列你的窗口.$DescriptionSubText" `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m1$X[0m' 以 $OptionsSubText`n$OptionsSubTextAgain"
	}
	ElseIf ($Option -eq "5"){ #DCloneTrackerSource
		$Options = @{
			"1" = "D2Emu.com"
			"2" = "D2runewizard.com"
			"3" = "diablo2.io"
		}
		$XMLChanged = OptionSubMenu -ConfigName "DCloneTrackerSource" -OptionsList $Options -Current $Script:Config.DCloneTrackerSource `
		-Description "选择 DClone 数据的 API 源.`n推荐 D2Emu.com 因为它直接从游戏中拉取数据." `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m1$X[0m' → D2Emu.com (推荐)`n    选择 '$X[38;2;255;165;000;22m2$X[0m' → D2runewizard.com`n    选择 '$X[38;2;255;165;000;22m3$X[0m' → diablo2.io`n"
	}
	ElseIf ($Option -eq "6"){ #DCloneAlarmList
		$Options = @{ # SCL-NA, SCL-EU, SCL-KR, SC-NA, SC-EU, SC-KR, HCL-NA, HCL-EU, HCL-KR, HC-NA, HC-EU, HC-KR
			"1" = "SCL-NA, SCL-EU, SCL-KR"
			"2" = "SC-NA, SC-EU, SC-KR"
			"3" = "HCL-NA, HCL-EU, HCL-KR"
			"4" = "HC-NA, HC-EU, HC-KR"
			"5" = "SCL-NA, SCL-EU, SCL-KR, SC-NA, SC-EU, SC-KR, HCL-NA, HCL-EU, HCL-KR, HC-NA, HC-EU, HC-KR"
			"6" = ""
		}
		$XMLChanged = OptionSubMenu -ConfigName "DCloneAlarmList" -OptionsList $Options -Current $(if ($Script:Config.DCloneAlarmList -eq ""){"Alarms are disabled"}Else{$Script:Config.DCloneAlarmList}) `
		-Description "使用这个选项来改变你想要 DClone 警报的游戏模式.`n这将选择所有区域对于给定的游戏模式. 要进行精细调整 (例如多个游戏模式), 直接编辑 config.xml." `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m1$X[0m' → 激活Softcore Ladder`n    选择 '$X[38;2;255;165;000;22m2$X[0m' → 激活Softcore Non-Ladder`n    选择 '$X[38;2;255;165;000;22m3$X[0m' → 激活Hardcore Ladder`n    选择 '$X[38;2;255;165;000;22m4$X[0m' → 激活Hardcore Non-Ladder`n    选择 '$X[38;2;255;165;000;22m5$X[0m' → 激活所有游戏模式`n    选择 '$X[38;2;255;165;000;22m6$X[0m' → 禁用所有警报`n"
		$Script:DCloneChangesCSV = $Null #Reset DClone tracking to remove old notifications appearing that may no longer be applicable.
		SetDCloneAlarmLevels
	}
	ElseIf ($Option -eq "7" -and $Script:Config.DCloneAlarmList -ne ""){ #DCloneAlarmLevel
		$Options = @{
			"1" = "All"
			"2" = "Close"
			"3" = "Imminent"
		}
		$XMLChanged = OptionSubMenu -ConfigName "DCloneAlarmLevel" -OptionsList $Options -Current $Script:Config.DCloneAlarmLevel `
		-Description "这个选项用于让你可以自定义你想要被警告的 DClone 状态变化如果你只想特定警报.`n请注意如果你设置为 Imminent 即将, 你可能会错过 DC Walk (soj 通常一次性大量出售)." `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m1$X[0m' → All（所有状态变化都提醒）`n    选择 '$X[38;2;255;165;000;22m2$X[0m' → Close（进度到 4/6 5/6 时提醒）`n    选择 '$X[38;2;255;165;000;22m3$X[0m' → Imminent（进度到 5/6 即将出现DC时提醒）`n"
		$Script:DCloneChangesCSV = $Null #Reset DClone tracking to remove old notifications appearing that may no longer be applicable.
		SetDCloneAlarmLevels
	}
	ElseIf ($Option -eq "8" -and $Script:Config.DCloneAlarmList -ne ""){ #DCloneAlarmVoice
		$Options = @{
			"1" = "Amazon"
			"2" = "Paladin"
		}
		$XMLChanged = OptionSubMenu -ConfigName "DCloneAlarmVoice" -OptionsList $Options -Current $Script:Config.DCloneAlarmVoice `
		-Description "这个选项用于让你可以改变 DClone 警报的语音风格." `
		-OptionsText "    选择 '$X[38;2;255;165;000;22m1$X[0m' → Amazon (女声)`n    选择 '$X[38;2;255;165;000;22m2$X[0m' → Paladin (男声)`n"
	}
	ElseIf ($Option -eq "9" -and $Script:Config.DCloneAlarmList -ne ""){ #DCloneAlarmVolume
		$XMLChanged = OptionSubMenu -ConfigName "DCloneAlarmVolume" -OptionInteger -Current $Script:Config.DCloneAlarmVolume `
		-Description "这个选项用于让你可以调整 DClone 警报的音量级别如果它们太响或太轻."
	}
	ElseIf ($Option -eq "t" -and $AllowTokenConfigMenuOption -eq $True){
		if ($Script:ForceAuthToken -ne $True){
			$Script:ForceAuthToken = $True
			Write-Host "  强制使用 AuthTokens $X[38;2;5;250;5;22m已激活$X[0m." -foregroundcolor yellow
			Write-Host "  认证时, 脚本现在将强制使用 AuthTokens (即使 accounts 已配置用户名密码等 AuthenticationMethod 配置)." -foregroundcolor green
		}
		else {
			$Script:ForceAuthToken = $False
			Write-Host "  强制使用 AuthTokens $X[38;2;255;165;000;22m已禁用$X[0m." -foregroundcolor yellow
			Write-Host "  认证时, 脚本现在将使用 accounts.csv 中的 AuthenticationMethod 列中的配置." -foregroundcolor green
		}
		start-sleep -milliseconds 3000
	}
	else {#go to main menu if no valid option was specified.
		return
	}
	if ($XMLChanged -eq $True){
		Write-Host "   Config Updated!" -foregroundcolor green
		ImportXML
		If ($Option -eq "4" -and $Script:Config.RememberWindowLocations -eq $True -and -not ($Script:AccountOptionsCSV | Get-Member -Name "WindowXCoordinates" -MemberType NoteProperty -ErrorAction SilentlyContinue)){#if this is the first time it's been enabled display a setup message
			Formatfunction -indents 2 -IsWarning -Text "`n你已经启用了 RememberWindowsLocations 但你还需要设置它. 要设置这个你需要执行以下步骤:"
			FormatFunction -indents 3 -iswarning -text "`n1. 打开所有你的 D2r 账户实例.`n2. 移动每个游戏实例的窗口到你喜欢的布局和大小."
			FormatFunction -indents 3 -iswarning -text "3. 返回这个选项菜单并进入 'RememberWindowLocations' 设置.`n4. 一旦进入这个菜单, 选择 's' 选项来保存任何打开的游戏实例的坐标."
			FormatFunction -indents 2 -iswarning -text  "`n`n现在当你打开这些账户时它们每次都会在这个屏幕位置打开 :)`n"
			PressTheAnyKey
		}
		start-sleep -milliseconds 2500
	}
}
Function Notifications {
	param (
		[bool] $Check
	)
	if ($Check -eq $True -and $Script:LastNotificationCheck -lt (Get-Date).addminutes(-30).ToString('yyyy.MM.dd HH:mm:ss')){#check for notifications once every 30mins
		try {
			$URI = "https://gitee.com/knightli3721/mengba-d2r/raw/master/references/D2RLoader-Notification.txt"
			$Script:Notifications = Invoke-RestMethod -Uri $URI
			if ($Notifications.notification -ne ""){
				if ($Script:PrevNotification -ne $Notifications.notification){#if message has changed since last check
					$Script:PrevNotification = $Notifications.notification
					$Script:NotificationHasBeenChecked = $False
					if ((get-date).tostring('yyyy.MM.dd HH:mm:ss') -lt $Notifications.ExpiryDate -and (get-date).tostring('yyyy.MM.dd HH:mm:ss') -gt $Notifications.PublishDate){
						$Script:NotificationsAvailable = $True
					}
				}
			}
			Else {
				$Script:NotificationsAvailable = $False
			}
			$Script:LastNotificationCheck = (get-date).tostring('yyyy.MM.dd HH:mm:ss')
		}
		Catch {
			Write-Debug "  无法检查提醒." # If this fails in production don't show any errors/warnings.
		}
	}
	ElseIf ($Check -eq $False){
		Write-Host
		formatfunction -text $Notifications.notification -indents 1
	}
	if ($Check -eq $True -and $Script:NotificationHasBeenChecked -eq $False -and $Script:NotificationsAvailable -eq $True){#only show message if user hasn't seen notification yet.
		Write-Host "                                              ⚠ $X[38;2;255;165;000;48;2;1;1;1;4m有新的提醒. 按 【i】 查看详情.$X[0m"
	}
}
Function D2rLevels {
	$Script:D2rLevels = @(
		@(1, "Rogue Encampment"),
		@(2, "Blood Moor"),
		@(3, "Cold Plains"),
		@(4, "Stony Field"),
		@(5, "Dark Wood"),
		@(6, "Black Marsh"),
		@(7, "Tamoe Highland"),
		@(8, "Den of Evil"),
		@(9, "Cave 1"),
		@(10, "Underground Passage 1"),
		@(11, "Hole 1"),
		@(12, "Pit 1"),
		@(13, "Cave 2"),
		@(14, "Underground Passage 2"),
		@(15, "Hole 2"),
		@(16, "Pit 2"),
		@(17, "Burial Grounds"),
		@(18, "Crypt"),
		@(19, "Mausoleum"),
		@(20, "Forgotten Tower"),
		@(21, "Tower Cellar 1"),
		@(22, "Tower Cellar 2"),
		@(23, "Tower Cellar 3"),
		@(24, "Tower Cellar 4"),
		@(25, "Tower Cellar 5"),
		@(26, "Monastery Gate"),
		@(27, "Outer Cloister"),
		@(28, "Barracks"),
		@(29, "Jail 1"),
		@(30, "Jail 2"),
		@(31, "Jail 3"),
		@(32, "Inner Cloister"),
		@(33, "Cathedral"),
		@(34, "Catacombs 1"),
		@(35, "Catacombs 2"),
		@(36, "Catacombs 3"),
		@(37, "Catacombs 4"),
		@(38, "Tristram"),
		@(39, "The Secret Cow Level"),
		@(40, "Lut Gholein"),
		@(41, "Rocky Waste"),
		@(42, "Dry Hills"),
		@(43, "Far Oasis"),
		@(44, "Lost City"),
		@(45, "Valley of Snakes"),
		@(46, "Canyon of the Magi"),
		@(47, "Sewers 1"),
		@(48, "Sewers 2"),
		@(49, "Sewers 3"),
		@(50, "Harem 1"),
		@(51, "Harem 2"),
		@(52, "Palace Cellar 1"),
		@(53, "Palace Cellar 2"),
		@(54, "Palace Cellar 3"),
		@(55, "Stony Tomb 1"),
		@(56, "Halls of the Dead 1"),
		@(57, "Halls of the Dead 2"),
		@(58, "Claw Viper Temple 1"),
		@(59, "Stony Tomb 2"),
		@(60, "Halls of the Dead 3"),
		@(61, "Claw Viper Temple 2"),
		@(62, "Maggot Lair 1"),
		@(63, "Maggot Lair 2"),
		@(64, "Maggot Lair 3"),
		@(65, "Ancient Tunnels"),
		@(66, "Tal Rashas Tomb 1"),
		@(67, "Tal Rashas Tomb 2"),
		@(68, "Tal Rashas Tomb 3"),
		@(69, "Tal Rashas Tomb 4"),
		@(70, "Tal Rashas Tomb 5"),
		@(71, "Tal Rashas Tomb 6"),
		@(72, "Tal Rashas Tomb 7"),
		@(73, "Tal Rashas Chamber"),
		@(74, "Arcane Sanctuary"),
		@(75, "Kurast Docks"),
		@(76, "Spider Forest"),
		@(77, "Great Marsh"),
		@(78, "Flayer Jungle"),
		@(79, "Lower Kurast"),
		@(80, "Kurast Bazaar"),
		@(81, "Upper Kurast"),
		@(82, "Kurast Causeway"),
		@(83, "Travincal"),
		@(84, "Archnid Lair"),
		@(85, "Spider Cavern"),
		@(86, "Swampy Pit 1"),
		@(87, "Swampy Pit 2"),
		@(88, "Flayer Dungeon 1"),
		@(89, "Flayer Dungeon 2"),
		@(90, "Swampy Pit 3"),
		@(91, "Flayer Dungeon 3"),
		@(92, "Sewers 1"),
		@(93, "Sewers 2"),
		@(94, "Ruined Temple"),
		@(95, "Disused Fane"),
		@(96, "Forgotten Reliquary"),
		@(97, "Forgotten Temple"),
		@(98, "Ruined Fane"),
		@(99, "Disused Reliquary"),
		@(100, "Durance of Hate 1"),
		@(101, "Durance of Hate 2"),
		@(102, "Durance of Hate 3"),
		@(103, "Pandemonium Fortress"),
		@(104, "Outer Steppes"),
		@(105, "Plains of Despair"),
		@(106, "City of the Damned"),
		@(107, "River of Flame"),
		@(108, "Chaos Sanctuary"),
		@(109, "Harrogath"),
		@(110, "Bloody Foothills"),
		@(111, "Frigid Highlands"),
		@(112, "Arreat Plateau"),
		@(113, "Crystalline Passage"),
		@(114, "Frozen River"),
		@(115, "Glacial Trail"),
		@(116, "Drifter Cavern"),
		@(117, "Frozen Tundra"),
		@(118, "The Ancients Way"),
		@(119, "Icy Cellar"),
		@(120, "Arreat Summit"),
		@(121, "Nihlathaks Temple"),
		@(122, "Halls of Anguish"),
		@(123, "Halls of Pain"),
		@(124, "Halls of Vaught"),
		@(125, "Abaddon"),
		@(126, "Pit of Acheron"),
		@(127, "Infernal Pit"),
		@(128, "Worldstone Keep 1"),
		@(129, "Worldstone Keep 2"),
		@(130, "Worldstone Keep 3"),
		@(131, "Throne of Destruction"),
		@(132, "Worldstone Keep")
	)
}
Function QuoteList {
$Script:QuoteList =
"Stay a while and listen..",
"My brothers will not have died in vain!",
"My brothers have escaped you...",
"Not even death can save you from me.",
"Good Day!",
"You have quite a treasure there in that Horadric Cube.",
"There's nothing the right potion can't cure.",
"Well, what the hell do you want? Oh, it's you. Uh, hi there.",
"Your souls shall fuel the Hellforge!",
"What do you need?",
"Your presence honors me.",
"I'll put that to good use.",
"Good to see you!",
"Looking for Baal?",
"All who oppose me, beware",
"Greetings",
"We live...AGAIN!",
"Ner. Ner! Nur. Roah. Hork, Hork.",
"Greetings, stranger. I'm not surprised to see your kind here.",
"There is a place of great evil in the wilderness.",
"East... Always into the east...",
"I shall make weapons from your bones",
"I am overburdened",
"This magic ring does me no good.",
"The siege has everything in short supply...except fools.",
"Beware, foul demons and beasts.",
"They'll never see me coming.",
"I will cleanse this wilderness.",
"I shall purge this land of the shadow.",
"I hear foul creatures about.",
"Ahh yes, ruins, the fate of all cities.",
"I'm never gunna give you up, never gunna let you down - Griswold, 1996.",
"I have no grief for him. Oblivion is his reward.",
"The catapults have been silenced.",
"The staff of kings, you astound me!",
"What's the matter, hero? Questioning your fortitude? I know we are.",
"This whole place is one big ale fog.",
"So, this is daylight... It's over-rated.",
"When - or if - I get to Lut Gholein, I'm going to find the largest bowl`nof Narlant weed and smoke 'til all earthly sense has left my body.",
"I've just about had my fill of the walking dead.",
"Oh I hate staining my hands with the blood of foul Sorcerers!",
"Damn it, I wish you people would just leave me alone!",
"Beware! Beyond lies mortal danger for the likes of you!",
"Beware! The evil is strong ahead.",
"Only the darkest Magics can turn the sun black.",
"You are too late! HAA HAA HAA",
"You now speak to Ormus. He was once a great mage, but now lives like a`nrat in a sinking vessel",
"I knew there was great potential in you, my friend. You've done a`nfantastic job.",
"Hi there. I'm Charsi, the Blacksmith here in camp. It's good to see some`nstrong adventurers around here.",
"Whatcha need?",
"Good day to you partner!",
"Moomoo, moo, moo. Moo, Moo Moo Moo Mooo.",
"Moo.",
"Moooooooooooooo",
"So cold and damp under the earth...",
"Good riddance, Blood Raven.",
"I shall meet death head-on.",
"The land here is dead and lifeless.",
"Let the gate be opened!",
"It is good to know that the sun shines again.",
"Maybe now the world will have peace.",
"Eternal suffering would be too brief for you, Diablo!",
"All that's left of proud Tristram are ghosts and ashes.",
"Ahh, the slow torture of caged starvation.",
"What a waste of undead flesh.",
"Good journey, Mephisto. Give my regards to the abyss.",
"My my, what a messy little demon!",
"Ah, the familiar scent of death.",
"What evil taints the light of the sun?",
"Light guide my way in this accursed place.",
"I shall honor Tal Rasha's sacrifice by destroying all the Prime Evils.",
"Oops...did I do that?",
"Death becomes you Andariel.",
"You dark mages are all alike, obsessed with power.",
"Planting the dead. How odd.",
"'Live, Laugh, Love' - Andariel, 1264.",
"Oh no, snakes. I hate snakes.",
"Who would have thought that such primitive beings could cause so much `ntrouble.",
"Hail to you champion",
"Help us! LET US OUT!",
"'I cannot carry anymore' - Me, carrying my teammates.",
"You're an even greater warrior than I expected...Sorry for `nunderestimating you.",
"Cut them down, warrior. All of them!",
"How can one kill what is already dead?",
"The Ancients must be close...",
"...That which does not kill you makes you stronger."
}
Function BannerLogo {
	if ($Script:IdleLimitForAccountUseTime -ne "" -and $Script:IdleLimitForAccountUseTime -gt 0 -and $Script:ActiveAccountsList.id.length -ne 0){#If Idlelimitfeature is used and there are active accounts, calc idle time to check if we should add idle status to banner
		$IdleText = ""
		$Script:IdleMinutes = [int](IdleTimeChecker)/60
		if ($IdleMinutes -gt $Script:IdleLimitForAccountUseTime){#If we've been idle longer than what's configured in config.xml then show a wee message under banner
			$IdleText = ("`n" +  $(if ([math]::floor($Script:IdleMinutes) -lt 10){' '}) + "    You have been idle for $([math]::floor($Script:IdleMinutes)) minute" + $(if ([math]::floor($Script:IdleMinutes) -gt 1){'s'}) + ". Hours Played will not be updated.")
		}
	}
	$BannerLogo = @"

    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%%%%%%%%%%%%%%%%%%%%
    %%%%%%%#%%%%%%%/%%%%%%%%%%%#%%%%%$Script:MOO%%%%##%%%%%%%%%#/##%%%%%%%%%%%%%%%%%%
    %%#(%/(%%%//(/*#%%%%%%%%%%%###%%%%#%%%%%###%%%%%###(*####%##%%%#%%*%%%%%%
    %%%( **/*///%%%%%%%%%###%%#(######%%#########%####/*/*.,#((#%%%#*,/%%%%%%
    %%%#*/.,*/,,*/#/%%%/(*#%%#*(%%(*/%#####%###%%%#%%%(///*,**/*/*(.&,%%%%%%%
    %%%%%%// % ,***(./*/(///,/(*,./*,*####%#*/#####/,/(((/.*.,.. (@.(%%%%%%%%
    %%%%%%%#%* &%%#..,,,,**,.,,.,,**///.*(#(*.,.,,*,,,,.,*, .&%&&.*%%%%%%%%%%
    %%%%%%%%%%%#.@&&%&&&%%%%&&%%,.(((//,,/*,,*.%&%&&&&&&&&%%&@%,#%%%%%%%%%%%%
    %%%%%%%%%%%%%(.&&&&&%&&&%(.,,*,,,,,.,,,,.,.*,%&&&%&&%&&&@*##%%%%%%%%%%%%%
    %%%%%%%%%%%%%%# @@@&&&&&(  @@&&@&&&&&&&&&*..,./(&&&&&&&&*####%%%%%%%%%%%%
    %%%%%%%%%%%%%%# &@@&&&&&(*, @@@&.,,,,. %@@&&*.,(%&&&&&&&/%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%#.&@@&&&&&(*, @@@@,((#&&%#.&@@&&.*#&&@&&&&/#%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%%*&@@@&&&&#*, @@@@,*(#%&&%#,@@@&@,(%&&&&&&(%%%%%%%%%%%%%%%%
    %%%%%$Script:MOO%%%%%%%*&@&&&%&&(,. @@@@,(%%%%%%#/,@@@& *#&&@&&%(%%%%%%%$Script:MOO%%%%%%
    %%%%%%%%%%%%%%%*&&&@&%&&(,. @@@@,%&%%%%%%(.@@@@ /#&&&&&&(%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%%,&&&&&%%&(*, @@@@,&&&&&&&%//@@@@./%&&&&@&(%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%(*&&&&&&&%(,, @@@@,%&&&#(/*.@@@@&./%&&&&@&(%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%%,&&&&&&&%(,, @@@@,/##/(// @@&@@,/#&&&&&&&(%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%(,&&&&&&&%(,, @@@@.*,,..*@@@&&*./#&&%&&&&&(%#%%#%%%%%%%%%%%
    %%%%%%%%%%%#%%#.&&&&&%%#* @@@&&&@@@&&%&&&% */*%&&%#&&&&&/((#%%%%%%%%%%%%%
    %%%%%%%%(#//*/.&&&#%#%#.@&& ..,,****,,*//((/*#%%%####%%%#/#/#%%%%%%%%%%%%
    %%%%%##***.,**////*(//,&.*/***.*/%%#%/%#*.***/*/***//**/(((/.,*(//*/(##%%
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$IdleText
"@
	if ($Script:PGemActivated -eq $True -or $Script:CowKingActivated -eq $True ){
		Write-Host "  $X[38;2;255;165;0;22m$BannerLogo$X[0m"
	}
	Else {
		Write-Host $BannerLogo -foregroundcolor yellow
	}
}
Function JokeMaster ([int]$JokeProviderRoll=""){
	#If you're going to leech and not provide any damage value in the Throne Room then at least provide entertainment value right?
	Write-Host "  Copy these mediocre jokes into the game while doing Baal Comedy Club runs`r`n  to mislead everyone into believing you have a personality:`n"
	do {
		$Joke = $Null
		$JokeType = "Joke"
		if ($JokeProviderRoll -eq "" -or $JokeProviderRoll -eq 69){
			$JokeProviderRoll = get-random -min 1 -max 3 #Randomly roll for first two joke providers
		}
		$attempt = 0
		do {
			$attempt ++
			if ($JokeProviderRoll -eq 1){
				if ((Get-Date).Month -eq 12 -or ((Get-Date).Month -eq 10 -and (Get-Date).Day -ge 24 -and (Get-Date).Day -le 31)){#If December or A week leading up to Halloween.
					if ((Get-Date).Month -eq 12){ #If December
						try {
							$Joke = Invoke-RestMethod -uri https://v2.jokeapi.dev/joke/Christmas -Method GET -header $headers -ErrorAction Stop #get absolutely punishing xmas jokes during December
							$JokeObtained = $true
						}
						catch {
							Write-Host "  Couldn't Reach v2.jokeapi.dev :( Trying alternate provider..." -foregroundcolor Red
							$JokeObtained = $false
							$JokeProviderRoll = 2
						}
					}
					Else {#else if it's around Halloweeen
						try {
							$Joke = Invoke-RestMethod -uri https://v2.jokeapi.dev/joke/Spooky -Method GET -header $headers -ErrorAction Stop #get absolutely punishing 'spooky' jokes in the week leading up to Halloween.
							$JokeObtained = $true
						}
						Catch {
							Write-Host "  Couldn't Reach v2.jokeapi.dev :( Trying alternate provider..." -foregroundcolor Red
							$JokeObtained = $false
							$JokeProviderRoll = 2
						}
					}
				}
				else {#For non seasonal jokes.
					try {
						$Joke = Invoke-RestMethod -uri "https://v2.jokeapi.dev/joke/Miscellaneous,Dark,Pun?blacklistFlags=racist,sexist" -Method GET -header $headers -ErrorAction Stop #exclude any racist/sexist jokes as we're not 75+ years old. Exclude IT/Programmer jokes that folk won't understand and also exclude, xmas & halloween themed jokes
						$JokeObtained = $true
					}
					catch {
						Write-Host "  Couldn't Reach v2.jokeapi.dev :( Trying alternate provider..." -foregroundcolor Red
						$JokeObtained = $false
						$JokeProviderRoll = 2
					}
				}
				if ($JokeObtained -eq $true){
					if ($Joke.Type -eq "twopart"){#some jokes are come through as two parters with two variables
						$JokeSetup = ($Joke.setup -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
						$JokeDelivery = ($Joke.delivery -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
						Write-Host "   $X[38;2;255;165;000;22m$JokeSetup$X[0m"
						Write-Host "   $X[38;2;255;165;000;22m$JokeDelivery$X[0m"
					}
					else {#else if single liner joke.
						$SingleJoke =  ($joke.joke -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
						Write-Host "   $X[38;2;255;165;000;22m$SingleJoke$X[0m"
					}
					$JokeProvider = "v2.jokeapi.dev"
				}
			}
			if ($JokeProviderRoll -eq 2){
				try {
					$Joke = Invoke-RestMethod -uri https://official-joke-api.appspot.com/random_joke -Method GET -header $headers -ErrorAction Stop
					$JokeObtained = $true
					$JokeSetup = ($Joke.setup -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
					$JokeDelivery = ($Joke.punchline -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
					Write-Host "   $X[38;2;255;165;000;22m$JokeSetup$X[0m"
					Write-Host "   $X[38;2;255;165;000;22m$JokeDelivery$X[0m"
					$JokeProvider = "official-joke-api.appspot.com"
				}
				catch {
					$JokeProviderRoll = 1
					Write-Host "  Couldn't Reach official-joke-api.appspot.com :( Trying alternate provider..." -foregroundcolor Red
				}
			}
			if ($JokeProviderRoll -eq 3){
				try {
					$headers = @{
						"User-Agent" = "github.com/shupershuff/Diablo2RLoader"
						"Accept" = "application/json"
					}
					$Joke = (Invoke-RestMethod -uri https://icanhazdadjoke.com -Method GET -header $headers -ErrorAction Stop).joke
					$JokeObtained = $true
					$SingleJoke =  ($joke -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
					Write-Host "   $X[38;2;255;165;000;22m$SingleJoke$X[0m"
					$JokeProvider = "icanhazdadjoke.com"
				}
				catch {
					$JokeProviderRoll = 1
					Write-Host "  Couldn't reach icanhazdadjoke.com :( API might be down." -foregroundcolor Red
				}
			}
			if ($JokeProviderRoll -eq 4){
				try {
					$Joke = (Invoke-RestMethod -uri https://api.chucknorris.io/jokes/random -ErrorAction Stop).value
					$JokeObtained = $true
					$SingleJoke =  ($joke -replace "(.{1,73})(?:\s|$)", "`n   `$1").trim() #add two spaces before any line breaks for indenting. Add line break for lines that are longer than 73 characters.
					Write-Host "   $X[38;2;255;165;000;22m$SingleJoke$X[0m"
					$JokeProvider = "api.chucknorris.io"
					$JokeType = "Fact"
				}
				catch {
					$JokeProviderRoll = 1
					Write-Host "  Couldn't reach api.chucknorris.io :( API might be down." -foregroundcolor Red
				}
			}
		} until ($JokeObtained -eq $true -or $attempt -eq 3)
		Write-Host
		if ($Null -ne $Joke){
			Write-Host "  $JokeType courtesy of $JokeProvider`n`n"
		}
		Write-Host "  Press '$X[38;2;255;165;000;22mj$X[0m' for more, '$X[38;2;255;165;000;22md$X[0m' for a Dad joke or '$X[38;2;255;165;000;22mc$X[0m' for a Chuck Norris fact."
		Write-Host "  Otherwise, press any other key to return to main menu... " -nonewline
		$JokeOption = readkey
		Write-Host;Write-Host
		if ($JokeOption -eq "j" -or $JokeOption -eq "d" -or $JokeOption -eq "c"){
			if ($JokeOption -eq "j"){
				$JokeProviderRoll = 69 #fish out another cringey joke.
			}
			if ($JokeOption -eq "d"){
				$JokeProviderRoll = 3
			}
			if ($JokeOption -eq "c"){
				$JokeProviderRoll = 4
			}
			# Clear faff from the console so more jokes show.
			$InputLength = 138
			$Host.UI.RawUI.CursorPosition = @{
				X = [Math]::Max($Host.UI.RawUI.CursorPosition.X - $InputLength, 0)
				Y = $Host.UI.RawUI.CursorPosition.Y -3
			}
			Write-Host -NoNewLine (" " * ($InputLength)) #-ForegroundColor Black
			$Host.UI.RawUI.CursorPosition = @{
				X = [Math]::Max($Host.UI.RawUI.CursorPosition.X - $InputLength, 0)
				Y = $Host.UI.RawUI.CursorPosition.Y -2
			}
		}
	} until ($JokeOption -ne "j" -and $JokeOption -ne "d" -and $JokeOption -ne "c")
}
Function DClone {# Display DClone Status.
	param (
		[bool] $DisableOutput,
		[String] $DCloneTrackerSource,
		[String] $Taglist,
		[object] $DCloneChanges,
		[String] $DCloneAlarmLevel,
		[String] $EmuToken
	)
	$RegionCNMap = $Script:RegionCNMap
	if (-not $RegionCNMap) { $RegionCNMap = @{ CN = "国服"; KR = "亚服"; NA = "美服"; EU = "欧服" } }
	$RegionOrder = $Script:RegionOrder
	if (-not $RegionOrder) { $RegionOrder = @{ CN = 0; KR = 1; NA = 2; EU = 3 } }
	if ($DCloneTrackerSource -eq "d2emu.com"){
		$headers = @{
			'x-emu-username' = 'shupershuff'
			'x-emu-token' = $EmuToken
		}
		$URI = "https://d2emu.com/api/v1/dclone"
		try {
			$D2RDCloneResponse = WebRequestWithTimeOut -InitiatingFunction "DClone" -DCloneSource $DCloneTrackerSource -ScriptBlock {
				Invoke-RestMethod -Uri $using:URI -Method GET -Headers $using:Headers
			} -TimeoutSeconds 4
			if ($DebugMode -eq $true) {
				Write-Host "Debug: DClone JSON Structure from d2emu.com:"
				$D2RDCloneResponse | ConvertTo-Json -Depth 3 | Write-Host
			}
			$D2RDCloneResponse = $D2RDCloneResponse.PSObject.Properties | ForEach-Object {
				[PSCustomObject]@{
					Name = $_.Name
					Progress = $_.Value.status
					Updated_at = $_.Value.updated_at
				}
			}
			$CurrentStatus = $D2RDCloneResponse | Select-Object @{Name='Server'; Expression={$_.name}},@{Name='Progress'; Expression={($_.Progress + 1)}}, @{Name='Updated'; Expression={$_.Updated_at}} #| sort server #add +1 as this source counts status from 0
		}
		Catch {#catch commands captured in WebRequestWithTimeOut function
			Write-Debug "Problem connecting to $URI"
		}
	}
	ElseIf ($DCloneTrackerSource -eq "D2runewizard.com"){
		$QLC = "zouaqcSTudL"
		$tokreg = ("QLC" + $qlc + 1 + "fnbttzP")
		$D2RWref = ""
		for ($i = $tokreg.Length - 1; $i -ge 0; $i--){
			$D2RWref += $tokreg[$i]
		}
		$headers = @{
			"D2R-Contact" = "placeholderemail@email.com"
			"D2R-Platform" = "GitHub"
			"D2R-Repo" = "https://github.com/shupershuff/Diablo2RLoader"
		}
		$URI = "https://d2runewizard.com/api/diablo-clone-progress/all?token=$D2RWref"
		try {
			$D2RDCloneResponse = WebRequestWithTimeOut -InitiatingFunction "DClone" -DCloneSource $DCloneTrackerSource -Headers -$headers -ScriptBlock {
				Invoke-RestMethod -Uri $using:URI -Method GET -Headers $using:Headers
			} -TimeoutSeconds 3
			if ($DebugMode -eq $true) {
				Write-Host "Debug: DClone JSON Structure from D2runewizard.com:"
				$D2RDCloneResponse | ConvertTo-Json -Depth 3 | Write-Host
			}
			$CurrentStatus = $D2RDCloneResponse.servers | Select-Object @{Name='Server'; Expression={$_.server}},@{Name='Progress'; Expression={$_.progress}}, @{Name='Updated'; Expression={$_.timestamp}} #| sort server
		}
		Catch {#catch commands captured in WebRequestWithTimeOut function
			Write-Debug "Problem connecting to $URI"
		}
	}
	ElseIf ($DCloneTrackerSource -eq "Diablo2.io"){
		$headers = @{
			"User-Agent" = "github.com/shupershuff/Diablo2RLoader"
		}
		$URI = "https://diablo2.io/dclone_api.php"
		try {
			$D2RDCloneResponse = WebRequestWithTimeOut -InitiatingFunction "DClone" -DCloneSource $DCloneTrackerSource -ScriptBlock {
				Invoke-RestMethod -Uri $using:URI -Method GET -Headers $using:Headers
			} -TimeoutSeconds 3
			$CurrentStatus = $D2RDCloneResponse | Select-Object @{Name='Server'; Expression={$_.region}},@{Name='Ladder'; Expression={$_.ladder}},@{Name='Core'; Expression={$_.hc}},@{Name='Progress'; Expression={$_.progress}}, @{Name='Updated'; Expression={$null}} # No time available
		}
		Catch {#catch commands captured in WebRequestWithTimeOut function
			Write-Debug "Problem connecting to $URI"
		}
	}
	Else {#if XML is invalid for DCloneTrackerSource
		$DCloneErrorMessage = ("  Error: Couldn't check for DClone Status. ###  Check DCloneTrackerSource in config.xml is entered correctly.").Replace("###", "`n")
		Write-Host
		Write-Host $DCloneErrorMessage -Foregroundcolor red
		if ($DisableOutput -ne $True){
			Write-Host
			$Script:AccountID = $null
			Presstheanykey
		}
		Return
	}
	if ($null -ne $DCloneErrorMessage){
		Write-Host $DCloneErrorMessage -Foregroundcolor red
		if ($DisableOutput -ne $True){
			Write-Host
			$Script:AccountID = $null
			Presstheanykey
		}
		Return
	}
	$DCloneLadderTable = New-Object -TypeName System.Collections.ArrayList
	$DCloneNonLadderTable = New-Object -TypeName System.Collections.ArrayList
	if ($DCloneChanges -eq "" -or $null -eq $DCloneChanges){
		$DCloneChangesArray = New-Object -TypeName System.Collections.ArrayList
	}
	Else {
		$DCloneChangesArray = $DCloneChanges | ConvertFrom-Csv -ErrorAction silentlycontinue #temporarily convert to array
	}
	ForEach ($Status in $CurrentStatus){
		$DCloneLadderInfo = New-Object -TypeName psobject
		$DCloneNonLadderInfo = New-Object -TypeName psobject
		#Convert data from all sources into consistent names and tags to be sorted and filtered.
		# 新: 统一区域代码并映射中文显示 (兼容 name/server/region 字段)
		$serverIdentifier = ""
		try { $serverIdentifier = ($Status.server).ToString().ToLower() } catch {}
		if (-not $serverIdentifier) {
			try { $serverIdentifier = ($Status.name).ToString().ToLower() } catch {}
		}
		if (-not $serverIdentifier) {
			try { $serverIdentifier = ($Status.region).ToString().ToLower() } catch {}
		}
		$serverCodeRaw = ""
		try { $serverCodeRaw = ($Status.Server).ToString() } catch {}
		$RegionCode = $null
		if ($serverIdentifier -match 'cn|china' -or $serverCodeRaw -eq "0") { $RegionCode = "CN" }
		elseif ($serverIdentifier -match 'na|us|america' -or $serverCodeRaw -eq "1") { $RegionCode = "NA" }
		elseif ($serverIdentifier -match 'eu|europe' -or $serverCodeRaw -eq "2") { $RegionCode = "EU" }
		elseif ($serverIdentifier -match 'kr|asia' -or $serverCodeRaw -eq "3") { $RegionCode = "KR" }
		else { $RegionCode = "NA" }
		$RegionDisplayName = $RegionCNMap[$RegionCode]; if (-not $RegionDisplayName) { $RegionDisplayName = $RegionCode }
		$TagSuffix = ("-" + $RegionCode)

		if (($Status.server -notlike "*nonladder*" -and -not [int]::TryParse($Status.server,[ref]$null)) -or $Status.Ladder -eq "1"){
			if ($Status.server -match "hardcore" -or $Status.Core -eq "1") { $Tag = ("HCL" + $TagSuffix); $ServerName = ("HCL - " + $RegionDisplayName) }
			else { $Tag = ("SCL" + $TagSuffix); $ServerName = ("SCL - " + $RegionDisplayName) }
			$DCloneLadderInfo | Add-Member -MemberType NoteProperty -Name Tag -Value $Tag
			$DCloneLadderInfo | Add-Member -MemberType NoteProperty -Name RegionCode -Value $RegionCode
			$DCloneLadderInfo | Add-Member -MemberType NoteProperty -Name LadderServer -Value $ServerName
			$DCloneLadderInfo | Add-Member -MemberType NoteProperty -Name LadderProgress -Value $Status.progress
			$DCloneLadderInfo | Add-Member -MemberType NoteProperty -Name Updated -Value $Status.Updated
			[VOID]$DCloneLadderTable.Add($DCloneLadderInfo)
		}
		Else {
			if ($Status.server -match "hardcore" -or $Status.Core -eq "1") { $Tag = ("HC" + $TagSuffix); $ServerName = ("HC - " + $RegionDisplayName) }
			else { $Tag = ("SC" + $TagSuffix); $ServerName = ("SC - " + $RegionDisplayName) }
			$DCloneNonLadderInfo | Add-Member -MemberType NoteProperty -Name Tag -Value $Tag
			$DCloneNonLadderInfo | Add-Member -MemberType NoteProperty -Name RegionCode -Value $RegionCode
			$DCloneNonLadderInfo | Add-Member -MemberType NoteProperty -Name NonLadderServer -Value $ServerName
			$DCloneNonLadderInfo | Add-Member -MemberType NoteProperty -Name NonLadderProgress -Value $Status.progress
			$DCloneNonLadderInfo | Add-Member -MemberType NoteProperty -Name Updated -Value $Status.Updated
			[VOID]$DCloneNonLadderTable.Add($DCloneNonLadderInfo)
		}
		if ($True -eq $DisableOutput){
			if ($taglist -match $Tag ){#if D Dclone region and server matches what's in config, check for changes.
				Write-Debug " Tag $tag in taglist" #debug
				if ($DCloneChangesArray | where-object {$_.Tag -eq $Tag}){
					ForEach ($Item in $DCloneChangesArray | where-object {$_.Tag -eq $Tag}){#for each tag specified in config.xml...
						$item.VoiceAlarmStatus = $False
						$item.TextAlarmStatus = $False
						if ($item.Status -ne $Status.progress){
							if ($Status.progress -ge 5 -and ($DCloneAlarmLevel -match 5)){#if DClone walk is imminent
								$item.VoiceAlarmStatus = $True
							}
							ElseIf ($Status.progress -eq 1 -or $Status.progress -eq 6){#if DClone walk has just happened
								$item.VoiceAlarmStatus = $True
							}
							ElseIf ($DCloneAlarmLevel -match $Status.progress){#if User has configured alarms to happen for lower DClone status levels (2,3,4)
								$item.VoiceAlarmStatus = $True
							}
							$item.TextAlarmStatus = $True
							if ($item.Status -ne "" -and $null -ne $item.Status){
								$item.PreviousStatus = $item.Status
							}
							$item.Status = $Status.progress
							$item.LastUpdate = (get-date).tostring('yyyy.MM.dd HH:mm:ss')
						}
						ElseIf ($Status.progress -eq 5){#if status hasn't changed, but status is 5 (imminent), show alarm text on main menu
							$item.TextAlarmStatus = $True
						}
						ElseIf ($Status.progress -lt 5 -and $item.LastUpdate -gt (get-date).addminutes(-5).ToString('yyyy.MM.dd HH:mm:ss')){#if status is less than 5 and has changed within the last 5 minutes, enable text alarm
							$item.TextAlarmStatus = $True
						}
						ElseIf ($null -ne $item.LastUpdate -and $item.LastUpdate -lt (get-date).addminutes(-5).ToString('yyyy.MM.dd HH:mm:ss')){#after 5 minutes remove the text alarm
							$item.LastUpdate = $null
							$item.TextAlarmStatus = $False
						}
					}
				}
				Else {
					try {
						if ($Status.progress -ge 5){
							$VoiceAlarmStatus = $True
							$TextAlarmStatus = $True
						}
						Else {
							$VoiceAlarmStatus = $False
							$TextAlarmStatus = $False
						}
						$DCloneChangesArray += [PSCustomObject]@{Tag = $Tag; PreviousStatus = $null; Status = $Status.progress; VoiceAlarmStatus = $VoiceAlarmStatus; TextAlarmStatus = $TextAlarmStatus; LastUpdate = $null}
					}
					Catch {#if script is refreshed too quick and $DCloneChangesArray gets corrupted, reset it.
						$DCloneChangesArray = $null
						Return
					}
				}
			}
		}
	}
	if ($True -ne $DisableOutput){
		$DCloneLadderTable = $DCloneLadderTable | Sort-Object @{ Expression = { $RegionOrder[$_.RegionCode] } }, LadderServer
		$DCloneNonLadderTable = $DCloneNonLadderTable | Sort-Object @{ Expression = { $RegionOrder[$_.RegionCode] } }, NonLadderServer
		$Count = 0
		Do {
			if ($Count -eq 0){
				Write-Host "                         黑毛状态:"
				Write-Host
				Write-Host " ###################################################################################"
				Write-Host " #                 赛季                   |                 非赛季                 #"
				Write-Host " #########################################|#########################################"
				Write-Host " #    服务器    进度(?/6)     更新于      |   服务器     进度(?/6)     更新于      #"
				Write-Host " #----------------------------------------|----------------------------------------#"
			}
			if ($Count -eq 2 -or $Count -eq 4 -or $Count -eq 6){
				Write-Host " #----------------------------------------|----------------------------------------#"
			}
			# 安全获取当前行对象，避免对 Null 数组进行索引
			$ladderObj = $null
			if ($DCloneLadderTable -and $DCloneLadderTable.Count -gt $Count) { $ladderObj = $DCloneLadderTable[$Count] }
			$nonObj = $null
			if ($DCloneNonLadderTable -and $DCloneNonLadderTable.Count -gt $Count) { $nonObj = $DCloneNonLadderTable[$Count] }

			$LadderServer = if ($ladderObj) { ($ladderObj.LadderServer).ToString() } else { "" }
			$LadderServer = PadRightDisplay $LadderServer 14
			$NonLadderServer = if ($nonObj) { ($nonObj.NonLadderServer).ToString() } else { "" }
			$NonLadderServer = PadRightDisplay $NonLadderServer 14

			$LadderUpdated = "     N/A    "
			$ladderUpdatedRaw = if ($ladderObj) { $ladderObj.Updated } else { $null }
			$parsedDate = New-Object DateTime
			if ($ladderUpdatedRaw -and [datetime]::TryParse($ladderUpdatedRaw, [ref]$parsedDate)) {
        $LadderUpdated = PadLeftDisplay ($parsedDate.ToString("MM/dd HH:mm")) 15
			} elseif ($ladderUpdatedRaw -and $ladderUpdatedRaw -match '^\d+$') {
				$unixSeconds = 0
				if ([long]::TryParse($ladderUpdatedRaw, [ref]$unixSeconds)) {
					$parsedDate = [DateTimeOffset]::FromUnixTimeSeconds($unixSeconds).ToLocalTime().DateTime
					$LadderUpdated = PadLeftDisplay ($parsedDate.ToString("MM/dd HH:mm")) 15
				}
			}

            $NonLadderUpdated = "     N/A    "
			$nonLadderUpdatedRaw = if ($nonObj) { $nonObj.Updated } else { $null }
			$parsedDate = New-Object DateTime
			if ($nonLadderUpdatedRaw -and [datetime]::TryParse($nonLadderUpdatedRaw, [ref]$parsedDate)) {
				$NonLadderUpdated = PadLeftDisplay ($parsedDate.ToString("MM/dd HH:mm")) 14
			} elseif ($nonLadderUpdatedRaw -and $nonLadderUpdatedRaw -match '^\d+$') {
				$unixSeconds = 0
				if ([long]::TryParse($nonLadderUpdatedRaw, [ref]$unixSeconds)) {
					$parsedDate = [DateTimeOffset]::FromUnixTimeSeconds($unixSeconds).ToLocalTime().DateTime
					$NonLadderUpdated = PadLeftDisplay ($parsedDate.ToString("MM/dd HH:mm")) 14
				}
			}

			$LadderProgressOut = if ($ladderObj) { ($ladderObj.LadderProgress).ToString() } else { " " }
			$NonLadderProgressOut = if ($nonObj) { ($nonObj.NonLadderProgress).ToString() } else { " " }
			Write-Host (" #  " + $LadderServer + $LadderProgressOut + "   " + $LadderUpdated + "   |  " + $NonLadderServer + $NonLadderProgressOut + "    " + $NonLadderUpdated + "   #")
			$Count = $Count + 1
			if ($Count -eq 8){
			#	Write-Host " #                                        |                                        #"
				Write-Host " ###################################################################################"
				Write-Host "`n   黑毛状态信息来自 $DCloneTrackerSource`n"
			}
		}
		Until ($Count -eq 8)
		PressTheAnyKey
	}
	ElseIf ($Taglist -ne "" -and $null -ne $DCloneChangesArray){#Else if Output is disabled and taglist has been specified, output dclone changes for alarm
		$DCloneChangesArray | ConvertTo-Csv -NoTypeInformation
	}
}
Function Set-PreferredVoice {
        param(
                [Parameter(Mandatory=$true)]
                [__ComObject] $SpVoice,
                [string] $Preference
        )
        try {
                $tokens = $SpVoice.GetVoices()
                if (-not $tokens -or $tokens.Count -eq 0) {
                        Write-Host "  未检测到任何可用的 SAPI 桌面语音." -ForegroundColor Yellow
                        return $false
                }
                $selected = $null
                switch -regex ($Preference) {
                        '^CN$' { $langHex = '804' }
                        '^(zh-CN)$' { $langHex = '804' }
                        '^(EN|EN-US)$' { $langHex = '409' }
                        default { $langHex = $null }
                }
                if ($langHex) {
                        $candidates = $SpVoice.GetVoices("Language=$langHex")
                        if ($candidates -and $candidates.Count -gt 0) {
                                $selected = $candidates.Item(0)
                        }
                }
                if (-not $selected -and $Preference) {
                        if ($Preference -in @('Bloke','Man','Paladin')) { $prefMatch = 'David' }
                        elseif ($Preference -in @('Wench','Woman','Amazon')) { $prefMatch = 'Zira' }
                        else { $prefMatch = $Preference }
                        for ($i = 0; $i -lt $tokens.Count; $i++) {
                                $t = $tokens.Item($i)
                                if ($t.GetDescription() -like "*$prefMatch*" -or $t.Id -like "*$prefMatch*") {
                                        $selected = $t
                                        break
                                }
                        }
                }
                if ($selected) {
                        $SpVoice.Voice = $selected
                        return $true
                }
                else {
                        Write-Host "  未找到匹配 '$Preference' 的语音，继续使用默认: $($SpVoice.Voice.GetDescription())" -ForegroundColor Yellow
                        return $false
                }
        }
        catch {
                Write-Host "  设置语音失败，继续使用默认: $($_.Exception.Message)" -ForegroundColor Yellow
                return $false
        }
}
Function Get-VoiceLanguage {
        param(
                [Parameter(Mandatory=$true)]
                [__ComObject] $SpVoice
        )
        try {
                $token = $SpVoice.Voice
                if ($null -eq $token) { return $null }
                try { $lang = $token.GetAttribute("Language") } catch { $lang = $null }
                if ($lang) {
                        if ($lang -match '(^|;|,|\s)804($|;|,|\s)') { return 'zh-CN' }
                        if ($lang -match '(^|;|,|\s)409($|;|,|\s)') { return 'en-US' }
                }
                $id = $token.Id
                $desc = $token.GetDescription()
                if ($id -match 'ZH[-_]?CN' -or $desc -match 'Chinese') { return 'zh-CN' }
                if ($id -match 'EN[-_]?US' -or $desc -match 'English') { return 'en-US' }
        } catch {}
        return $null
}
Function Invoke-LocalizedSpeech {
	param(
			[Parameter(Mandatory=$true)]
			[__ComObject] $SpVoice,
			[string] $MessageZhCN,
			[Parameter(Mandatory=$true)]
			[string] $MessageEnUS,
			[string] $Preference,
			[int] $Rate,
			[int] $Volume
	)
	try {
			if ($PSBoundParameters.ContainsKey('Rate')) { $SpVoice.Rate = $Rate }
			if ($PSBoundParameters.ContainsKey('Volume')) { $SpVoice.Volume = $Volume }
			if ($Preference) { [void](Set-PreferredVoice -SpVoice $SpVoice -Preference $Preference) }
			$lang = Get-VoiceLanguage -SpVoice $SpVoice
			$textToSpeak = if ($lang -eq 'zh-CN' -and $MessageZhCN) { $MessageZhCN } else { $MessageEnUS }
			$SpVoice.Speak($textToSpeak) | Out-Null
			return $true
	}
	catch {
			Write-Host "  语音播报失败: $($_.Exception.Message)" -ForegroundColor Yellow
			return $false
	}
}
Function Test-DCloneTTS {
        param(
                [string] $Preference
        )
        $voice = New-Object -ComObject Sapi.spvoice
        $rate = -2
        $volume = 100
        try { if ($Script:Config -and $Script:Config.DCloneAlarmVolume) { $volume = $Script:Config.DCloneAlarmVolume } } catch {}
        $voice.Rate = $rate
        $voice.Volume = $volume
        [void](Set-PreferredVoice -SpVoice $voice -Preference $Preference)
        $lang = Get-VoiceLanguage -SpVoice $voice
        try { $desc = $voice.Voice.GetDescription() } catch { $desc = "(unknown)" }
        Write-Host "[TTS] 语音: $desc | 语言: $lang | 偏好: '$Preference' | 速率: $rate | 音量: $volume" -ForegroundColor Cyan
        Invoke-LocalizedSpeech -SpVoice $voice -Preference $Preference -Rate $rate -Volume $volume -MessageZhCN "黑毛快出了！ 服务器【国服】，模式【普通模式 赛季】" -MessageEnUS "D Clone Imminent! DClone is about to walk in CN on Softcore Ladder"
        Start-Sleep -Milliseconds 300
        Invoke-LocalizedSpeech -SpVoice $voice -Preference $Preference -Rate $rate -Volume $volume -MessageZhCN "黑毛已经出了！ 服务器【美服】，模式【专家模式 非赛季】" -MessageEnUS "D Clone has just walked in NA on Hardcore Non-Ladder"
        Start-Sleep -Milliseconds 300
        Invoke-LocalizedSpeech -SpVoice $voice -Preference $Preference -Rate $rate -Volume $volume -MessageZhCN "黑毛状态更新！ 当前进度 3/6，服务器【欧服】，模式【普通模式 赛季】" -MessageEnUS "D Clone is now 3 in EU on Softcore Ladder"
}
Function DCloneVoiceAlarm {
	$voice = New-Object -ComObject Sapi.spvoice
	$voice.rate = -2 #How quickly the voice message should be
	$voice.volume = $Config.DCloneAlarmVolume
	Write-Host
	[void](Set-PreferredVoice -SpVoice $voice -Preference $Script:Config.DCloneAlarmVoice)
	ForEach ($Item in ($Script:DCloneChangesCSV | ConvertFrom-Csv) | where-object {$_.VoiceAlarmStatus -Match "True" -or $_.TextAlarmStatus -Match "True"}){
		if ($item.tag -match "l"){#if mode contains "L"
			$LadderText = "赛季"
		}
		else {#else it's non ladder
			$LadderText = "非赛季"
		}
		$LadderTextEN = "Ladder"; if ($LadderText -ne "赛季") { $LadderTextEN = "Non-Ladder" }
		if ($item.tag -match "HC"){
			$CoreText = "专家模式"
		}
		Else {#if mode contains "SC"
			$CoreText = "普通模式"
		}
		$CoreTextEN = if ($CoreText -eq "专家模式") { "Hardcore" } else { "Softcore" }
		if ($item.tag -match "CN"){
			$DCloneRegion = "国服"
		}
		ElseIf ($item.tag -match "NA"){
			$DCloneRegion = "美服"
		}
		ElseIf ($item.tag -match "KR"){
			$DCloneRegion = "亚服"
		}
		else {
			$DCloneRegion = "欧服"
		}
		if ($Item.Status -eq 5){
			Write-Host "  $X[38;2;165;146;99;48;2;1;1;1;4m黑毛快出了！ 服务器【$DCloneRegion】，模式【$CoreText $LadderText】 ($($item.tag))!$X[0m"
			$MessageZhCN = ("黑毛快出了！ 服务器【$DCloneRegion】，模式【$CoreText $LadderText】")
			$MessageEnUS = ("D Clone Imminent! DClone is about to walk in $DCloneRegion on " + $CoreTextEN + " " + $LadderTextEN)
			$Message = $MessageEnUS
		}
		ElseIf (($Item.Status -eq 1 -and $Item.PreviousStatus -ne 6) -or $Item.Status -eq 6){#check if status has just changed to 6 or it has changed to 1 from any number other than 6 (to prevent duplicate alarms.
			Write-Host "  $X[38;2;165;146;99;48;2;1;1;1;4m黑毛已经出了！ 服务器【$DCloneRegion】，模式【$CoreText $LadderText】 ($($item.tag))。$X[0m"
			$MessageZhCN = ("黑毛已经出了！ 服务器【$DCloneRegion】，模式【$CoreText $LadderText】")
			$MessageEnUS = ("D Clone has just walked in $DCloneRegion on " + $CoreTextEN + " " + $LadderTextEN)
			$Message = $MessageEnUS
		}
		ElseIf ($Script:DCloneAlarmLevel -match $Item.Status){
			Write-Host "  $X[38;2;165;146;99;48;2;1;1;1;4mDClone Update! DClone is now $($Item.Status)/6 in $DCloneRegion on $CoreText $LadderText ($($item.tag))$X[0m"
			$MessageZhCN = ("黑毛状态更新！ 当前进度 $($Item.Status)/6，服务器【$DCloneRegion】，模式【$CoreText $LadderText】")
			$MessageEnUS = ("D Clone is now " + $Item.Status + " in $DCloneRegion on " + $CoreTextEN + " " + $LadderTextEN)
			$Message = $MessageEnUS
		}
		if ($item.VoiceAlarmStatus -eq $True){
			Invoke-LocalizedSpeech -SpVoice $voice -MessageZhCN $MessageZhCN -MessageEnUS $MessageEnUS
		}
	}
	if ($null -ne $Message){
		Write-Host "  $X[38;2;065;105;225;48;2;1;1;1;4m黑毛状态信息来自 $($Script:Config.DCloneTrackerSource)$X[0m"
	}
}
Function WebRequestWithTimeOut {#Used to timeout web requests that take too long.
	param (
		[ScriptBlock] $ScriptBlock,
		[int] $TimeoutSeconds,
		[String] $InitiatingFunction,
		[String] $DCloneSource
	)
	$Script:DCloneErrorMessage = $null
	$TimedJob = Start-Job -ScriptBlock $ScriptBlock
	$timer = [Diagnostics.Stopwatch]::StartNew()
	while ($TimedJob.State -eq "Running" -and $timer.Elapsed.TotalSeconds -lt $TimeoutSeconds){
		Start-Sleep -Milliseconds 10
	}
	if ($TimedJob.State -eq "Running"){
		Stop-Job -Job $TimedJob
		if ($InitiatingFunction -eq "DClone"){
			$Script:DCloneErrorMessage = "   错误: 无法连接到 $DCloneTrackerSource 获取最新黑毛状态."
			Write-Host ("`n   连接超时: 无法连接到 $DCloneTrackerSource 获取最新黑毛状态.") -foregroundcolor red
			Throw "连接超时 :(" #force an exception to break out of the try statement.
			Write-Host
		}
	}
	ElseIf ($TimedJob.State -eq "Completed"){
		$result = Receive-Job -Job $TimedJob
		Write-Verbose "命令成功完成."
		ForEach ($Object in $result){#Remove Properties from Result Array inserted by the Start-Job command. This prevents skewed data for DClone status checks.
			$Object.PSObject.Properties.Remove("RunspaceId")
			$Object.PSObject.Properties.Remove("PSComputerName")
			$Object.PSObject.Properties.Remove("PSShowComputerName")
		}
		$result
	}
	else {
		if ($InitiatingFunction -eq "DClone"){
			Write-Host " 无法连接到 $DCloneSource." -foregroundcolor red
		}
	}
	Remove-Job -Job $TimedJob
}
Function TerrorZone {
	# Get the current time data was pulled
	$TimeDataObtained = (Get-Date -Format 'h:mmtt')
	$TZProvider = "D2Emu.com"
	$TZURI = "https://www.d2emu.com/api/v1/tz"
	$headers = @{
		'x-emu-username' = 'shupershuff'
		'x-emu-token' = $EmuToken
	}
	try {
		$D2TZResponse = Invoke-RestMethod -Uri $TZURI -headers $headers -TimeoutSec 5
	}
	catch {
		FormatFunction -Indents 3 -IsError -Text "无法从 $TZProvider 获取 TZ 数据`n"
		PressTheAnyKey
		return
	}
	ForEach ($Level in $D2TZResponse.current){
		Write-Debug "Level ID is: $Level"
		ForEach ($LevelID in $D2rLevels){
			if ($LevelID[0] -eq $Level){
				$CurrentTZ += $LevelID[1] + ", "
			}
		}
	}
	$CurrentTZ = $CurrentTZ -replace '..$', ''
	Function GetImmunities {
		param (
			[string]$ImmunityKey
		)
		$KeyName = "${ImmunityKey}_immunities"  # Construct the dynamic key name
		$Immunities = New-Object -TypeName System.Collections.ArrayList
		$TidiedImmunities = New-Object -TypeName System.Collections.ArrayList
		Foreach ($Immunity in $D2TZResponse.$KeyName){  # Use the dynamic key
			#The Immunity data isn't always consistent, sometimes it's 'c' for cold or sometimes it's "cold". Let's tidy it up.
			if ($Immunity -eq "p"){
				$Immunity = "Poison"
			}
			if ($Immunity -eq "ph"){
				$Immunity = "Physical"
			}
			if ($Immunity -eq "m"){
				$Immunity = "Magic"
			}
			if ($Immunity -eq "c"){
				$Immunity = "Cold"
			}
			if ($Immunity -eq "l"){
				$Immunity = "Lightning"
			}
			if ($Immunity -eq "f"){
				$Immunity = "Fire"
			}
			$Immunities += $Immunity
		}
		$Immunities = $Immunities | sort-object
		Foreach ($Immunity in $Immunities) {
			#Set some relevant colours for each of the resistances
			if ($Immunity -eq "Poison"){
				$Immunity = "$X[38;2;0;225;0;22mPoison$X[0m"
			}
			if ($Immunity -eq "Physical"){
				$Immunity = "$X[38;2;165;146;99;22mPhysical$X[0m"
			}
			if ($Immunity -eq "Magic"){
				$Immunity = "$X[38;2;165;165;164;22mMagic$X[0m"
			}
			if ($Immunity -eq "Cold"){
				$Immunity = "$X[38;2;65;105;225;22mCold$X[0m"
			}
			if ($Immunity -eq "Lightning"){
				$Immunity = "$X[38;2;255;255;0;22mLightning$X[0m"
			}
			if ($Immunity -eq "Fire"){
				$Immunity = "$X[38;2;255;10;0;22mFire$X[0m"
			}
			$TidiedImmunities += $Immunity
		}
		$TidiedImmunities = $TidiedImmunities | select-object -unique # Get rid of any duplicate entries.
		$TidiedImmunities = $TidiedImmunities -join ", "
		return $TidiedImmunities
	}
	write-host "   $X[38;2;255;165;000;4mCurrent Terror Zone Details:$X[0m"
	FormatFunction -SubsequentLineIndents 22 -text ("  Terrorized Level$(if($D2TZResponse.current.count -gt 1){'s:'}else{': '})  $X[38;2;255;0;255;22m" + $CurrentTZ + "$X[0m")
	FormatFunction -SubsequentLineIndents 22 -text ("  Immunities:         $X[38;2;255;0;255;22m" + (GetImmunities "Current") + "$X[0m")
	FormatFunction -SubsequentLineIndents 22 -text ("  Boss Packs:         Between $X[38;2;255;0;255;22m" + $D2TZResponse.current_num_boss_packs[0] + "$X[0m" + " and $X[38;2;255;0;255;22m" + $D2TZResponse.current_num_boss_packs[1] + "$X[0m boss packs $(if($D2TZResponse.current.count -gt 1){'across all TZ levels.'}else{'within this TZ level.'})")
	if ($D2TZResponse.current_superuniques.count -ge 1){
		FormatFunction -SubsequentLineIndents 22 -text ("  SuperUniques:       $X[38;2;165;146;99;22m" + ($D2TZResponse.current_superuniques -join ", ") + "$X[0m")
	}
	Else {
		FormatFunction -SubsequentLineIndents 22 -text "  SuperUniques:       No Super Unique monsters for $(if($D2TZResponse.current.count -gt 1){'these TZ levels'}else{'this TZ level'})."
	}
	write-host
	write-host "   $X[38;2;255;165;000;4mNext Terror Zone Details:$X[0m"
	if (($D2TZResponse.next_available_time_utc + 1) -lt [int]((Get-Date).ToUniversalTime() - [datetime]"1970-01-01 00:00:00").TotalSeconds){
		ForEach ($Level in $D2TZResponse.next){
			Write-Debug "Level ID is: $Level"
			ForEach ($LevelID in $D2rLevels){
				if ($LevelID[0] -eq $Level){
					$NextTZ += $LevelID[1] + ", "
				}
			}
		}
		$NextTZ = $NextTZ -replace '..$', ''
		FormatFunction -SubsequentLineIndents 22 -text ("  Terrorized Level$(if($D2TZResponse.next.count -gt 1){'s:'}else{': '})  $X[38;2;255;0;255;22m" + $NextTZ + "$X[0m")
		FormatFunction -SubsequentLineIndents 22 -text ("  Immunities:         $X[38;2;255;0;255;22m" + (GetImmunities "Next") + "$X[0m")
		FormatFunction -SubsequentLineIndents 22 -text ("  Boss Packs:         Between $X[38;2;255;0;255;22m" + $D2TZResponse.next_num_boss_packs[0] + "$X[0m" + " and $X[38;2;255;0;255;22m" + $D2TZResponse.next_num_boss_packs[1] + "$X[0m boss packs $(if($D2TZResponse.next.count -gt 1){'across all TZ levels.'}else{'within this TZ level.'})")
		if ($D2TZResponse.next_superuniques.count -ge 1){
			FormatFunction -SubsequentLineIndents 22 -text ("  SuperUniques:       $X[38;2;165;146;99;22m" + ($D2TZResponse.next_superuniques -join ", ") + "$X[0m")
		}
		Else {
			FormatFunction -SubsequentLineIndents 22 -text "  SuperUniques:       No Super Unique monsters for $(if($D2TZResponse.next.count -gt 1){'these TZ levels'}else{'this TZ level'})."
		}
	}
	else {
		$MinutesToWait = [int]([math]::Abs($D2TZResponse.next_available_time_utc - ([int]((Get-Date).ToUniversalTime() - [datetime]"1970-01-01 00:00:00").TotalSeconds)) / 60)
		if ($MinutesToWait -gt 1){
			$Plural = "s"
		}
		FormatFunction -SubsequentLineIndents 22 -text ("  Terrorized Level:   $X[38;2;255;0;255;22mNext TZ info is not available from TZ Provider API for another $MinutesToWait minute$Plural.$X[0m")
		if ($D2TZResponse.next_superuniques.count -ge 1){
			FormatFunction -SubsequentLineIndents 22 -text ("  SuperUniques:       $X[38;2;165;146;99;22m" + ($D2TZResponse.next_superuniques -join ", ") + "$X[0m")
		}
		Else {
			FormatFunction -SubsequentLineIndents 22 -text "  SuperUniques:       No Super Unique monsters for $(if($D2TZResponse.next.count -gt 1){'these TZ levels'}else{'this TZ level'})."
		}
	}
	if ($Debug){
		$D2TZResponse
		Write-Host "Current Immunities data"
		$D2TZResponse.current_immunities
	}
	Write-Host "`n  Information Retrieved at: " $TimeDataObtained
	Write-Host "  TZ info courtesy of:       $TZProvider`n"
	PressTheAnyKey
}
Function KillHandle { #Thanks to sir-wilhelm for tidying this up.
	param ([Switch]$TryAgain)
	$handle64 = "$PSScriptRoot\handle\handle64.exe"
	$handle = & $handle64 -accepteula -a -p D2R.exe "Check For Other Instances" -nobanner | Out-String
	if ($TryAgain -and $Handle -match "no matching handles found"){ #If handle can't be found, perhaps the game is slow to load, try waiting half a second before trying again.
		do {
			$attempt ++
			write-debug "Handle wasn't found, trying again in 500ms..."
			start-sleep -milliseconds 500
			$handle = & $handle64 -accepteula -a -p D2R.exe "Check For Other Instances" -nobanner | Out-String
		} until ($attempt -eq 3 -or $Handle -notmatch "no matching handles found")
	}
	if ($handle -match "pid:\s+(?<d2pid>\d+)\s+type:\s+Event\s+(?<eventHandle>\w+):"){
		$d2pid = $matches["d2pid"]
		$eventHandle = $matches["eventHandle"]
		Write-Verbose "Closing handle: $eventHandle on pid: $d2pid"
		& $handle64 -c $eventHandle -p $d2pid -y #-nobanner
	}
}
Function CheckActiveAccounts {#Note: only works for accounts loaded by the script
	#check if there's any open instances and check the game title window for which account is being used.
	try {
		$Script:ActiveIDs = $Null
		$D2rRunning = $false
		$Script:ActiveIDs = New-Object -TypeName System.Collections.ArrayList
		$Script:ActiveIDs = (Get-Process | Where-Object {$_.processname -eq "D2r" -and $_.MainWindowTitle -match "- Diablo II: Resurrected$"} | Select-Object MainWindowTitle).mainwindowtitle.substring(0,2).trim() #find all diablo 2 game windows and pull the account ID from the title
		$Script:D2rRunning = $true
		Write-Verbose "当前有运行中的 D2R 实例."
	}
	catch {#if the above fails then there are no running D2r instances.
		$Script:D2rRunning = $false
		Write-Verbose "当前没有运行中的 D2R 实例."
		$Script:ActiveIDs = ""
	}
	if ($Script:D2rRunning -eq $True){
		$Script:ActiveAccountsList = New-Object -TypeName System.Collections.ArrayList
		ForEach ($ActiveID in $ActiveIDs){#Build list of active accounts that we can omit from being selected later
			$ActiveAccountDetails = $Script:AccountOptionsCSV | where-object {$_.id -eq $ActiveID}
			$ActiveAccount = New-Object -TypeName psobject
			$ActiveAccount | Add-Member -MemberType NoteProperty -Name ID -Value $ActiveAccountDetails.ID
			$ActiveAccount | Add-Member -MemberType NoteProperty -Name AccountName -Value $ActiveAccountDetails.accountlabel
			$InstanceProcessID = (Get-Process | Where-Object {$_.processname -eq "D2r" -and ($_.MainWindowTitle -match "$($ActiveAccountDetails.ID) - $($ActiveAccountDetails.accountlabel)" -and $_.MainWindowTitle -match "- Diablo II: Resurrected$")} | Select-Object ID).id
			write-verbose "  $($ActiveAccountDetails.ID) - $($ActiveAccountDetails.accountlabel) 的进程ID为 $InstanceProcessID"
			$ActiveAccount | Add-Member -MemberType NoteProperty -Name ProcessID -Value $InstanceProcessID
			[VOID]$Script:ActiveAccountsList.Add($ActiveAccount)
		}
	}
	else {
		$Script:ActiveAccountsList = $Null
	}
}
Function DisplayActiveAccounts {
	Write-Host
	if ($Script:EnableBatchFeature -eq $true){
		$labels = $Script:AccountOptionsCSV | ForEach-Object { $_.accountlabel }
		$LongestAccountLabelLength = ($labels | ForEach-Object { if ($_){ GetDisplayWidth $_ } else { 0 } } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum)
		if ($LongestAccountLabelLength -ge 28){$LongestAccountLabelLength = 28} # limit very long names for layout sanity
		$AccountHeaderIndent = ""
		$__need = [Math]::Max(0, $LongestAccountLabelLength - 13)
		if ($__need -gt 0){ $AccountHeaderIndent = (" " * $__need) }
	}
	if ($Script:ActiveAccountsList.id -ne ""){#if batch feature is enabled add a column to display batch number(s)
		if ($Script:Config.TrackAccountUseTime -eq $true){
			$PlayTimeHeader = "  游玩时间(H)   "
		}
		if ($Script:EnableBatchFeature -eq $true){
			foreach ($row in $Script:AccountOptionsCSV) {
				if ($row.batches -like '*,*'){
					$Plural = 'es'
					break
				}
			}
			$BatchesHeader = ("" + $AccountHeaderIndent + "Batch$Plural")
		}
		# 使用显示宽度函数避免中文全角导致的列错位
		Write-Host ("   序号   区服   " + $PlayTimeHeader + "账号标签   " + $BatchesHeader) #Header
	}
	else {
		Write-Host "   序号  Account Label"
	}
	$Pattern = "(?<=\()([a-z]+)(?=\.actual\.battle.*\))" #Regex pattern to pull the region characters out of the window title.
	ForEach ($AccountOption in ($Script:AccountOptionsCSV | Sort-Object -Property @{ #Try sort by number first (needed for 2 digit ID's), then sort by character.
		Expression = {
			$intValue = [int]::TryParse($_.ID, [ref]$null) # Try to convert the value to an integer
			if ($intValue){# If it's not null then it's a number, so return it as an integer for sorting.
				[int]$_.ID
			}
			else {# If it's not a number, return a character and sort that.
				[char]$_.ID
			}
		}
	}))
	{
		if ($AccountOption.accountlabel.length -gt 28){ #later we ensure that strings longer than 28 chars are cut short so they don't disrupt the display.
			$AccountOption.accountlabel = $AccountOption.accountlabel.Substring(0, 28)
		}
		$RegionDisplayPostIndent = ""
		$RegionDisplayPreIndent = ""
		if ($AccountOption.ID.length -ge 2){#keep table formatting looking lovely if some crazy user has 10+ accounts.
			$IDIndent = ""
		}
		else {
			$IDIndent = " "
		}
		if ($Script:EnableBatchFeature -eq $true){
			$Batches = $AccountOption.batches
			if ($LongestAccountLabelLength -lt 13){ $LongestAccountLabelLength = 13 }
			$labelW = GetDisplayWidth $AccountOption.accountlabel
			$pad = [Math]::Max(0, $LongestAccountLabelLength - $labelW)
			$AccountIndent = (" " * $pad)
		}
		if ($Script:Config.TrackAccountUseTime -eq $true){
			try {
				$AcctPlayTime = (" " + (($time =([TimeSpan]::Parse($AccountOption.TimeActive))).hours + ($time.days * 24)).tostring() + ":" + ("{0:D2}" -f $time.minutes) + "   ")  # Add hours + (days*24) to show total hours, then show ":" followed by minutes
			}
			catch {#if account hasn't been opened yet.
				$AcctPlayTime = "   0   "
				Write-Debug "账号尚未打开."
			}
      $AcctPlayTime = PadLeftDisplay $AcctPlayTime 11
		}
		if ($AccountOption.id -in $Script:ActiveAccountsList.id){ #if account is currently active
			$Windowname = (Get-Process | Where-Object {$_.processname -eq "D2r" -and $_.MainWindowTitle -like ($AccountOption.id + "*Diablo II: Resurrected")} | Select-Object MainWindowTitle).mainwindowtitle #Check active game instances to see which accounts are active. As this is based on checking window titles, this will only work for accounts opened from the script
			$CurrentRegion = [regex]::Match($WindowName, $Pattern).value #Check which region aka realm the active account is connected to.
			if ($CurrentRegion -eq "CN"){$CurrentRegion = "国服"; $RegionDisplayPreIndent = " "; $RegionDisplayPostIndent = " "}
			if ($CurrentRegion -eq "US"){$CurrentRegion = "美服"; $RegionDisplayPreIndent = " "; $RegionDisplayPostIndent = " "}
			if ($CurrentRegion -eq "KR"){$CurrentRegion = "亚服"; $RegionDisplayPreIndent = " "; $RegionDisplayPostIndent = " "}
			if ($CurrentRegion -eq "EU"){$CurrentRegion = "欧服"; $RegionDisplayPreIndent = " "; $RegionDisplayPostIndent = " "}
			$labelOut = PadRightDisplay $AccountOption.accountlabel $LongestAccountLabelLength
			Write-Host (" " + $IDIndent + "【" + $AccountOption.ID + "】  "  + $RegionDisplayPreIndent + $CurrentRegion + $RegionDisplayPostIndent + "  " + $AcctPlayTime + ("     " + $labelOut) + " - Account Active.") -foregroundcolor yellow
		}
		else {#if account isn't currently active
			$labelOut = PadRightDisplay $AccountOption.accountlabel $LongestAccountLabelLength
			Write-Host (" " + $IDIndent + "【" + $AccountOption.ID + "】    -     " + $AcctPlayTime + ("     " + $labelOut) + "  " + $AccountIndent + $Batches) -foregroundcolor green
		}
	}
}
Function Menu {
	Clear-Host
	if ($Script:ScriptHasBeenRun -eq $true){
		$Script:AccountUsername = $Null
		if ($DebugMode -eq $true){
			DisplayPreviousAccountOpened
		}
	}
	Else {
		Write-Host ("  圣光啊 你有看到那个赫拉迪克多开脚本吗？ v" + $Currentversion)
	}
	Notifications -check $True
	BannerLogo
	QuoteRoll
	if ($Null -eq $Batch -and $Script:OpenAllAccounts -ne $True){#go through normal account selection screen if script hasn't been launched with parameters that already determine this.
		ChooseAccount
	}
	Else {
		CheckActiveAccounts
		$Script:PWmanualset = $False
		$Script:AcceptableValues = New-Object -TypeName System.Collections.ArrayList
		$Script:TwoDigitIDsUsed = $False
		$Script:TwoDigitBatchesUsed = $False
		ForEach ($AccountOption in $Script:AccountOptionsCSV){
			if ($AccountOption.id -notin $Script:ActiveAccountsList.id){
				$Script:AcceptableValues = $AcceptableValues + ($AccountOption.id) #+ "x"
				if ($AccountOption.id.length -eq 2){
					$Script:TwoDigitIDsUsed = $True
				}
			}
		}
	}
	if ($Null -ne $Batch -or $Script:OpenBatches -eq $true){#if batch has been passed through parameter or if batch was been selected from the menu.
		$Script:AcceptableBatchIDs = $Null #reset value
		$AcceptableBatchValues = $Null #reset value
		ForEach ($ID in $Script:AccountOptionsCSV){
			if ($ID.id -in $Script:AcceptableValues){#Find batch values to choose from based on accounts that aren't already open.
				$AcceptableBatchValues = $AcceptableBatchValues + ($ID.batches).split(',') #collate acceptable options of batch ID's
				$Script:AcceptableBatchIDs = $Script:AcceptableBatchIDs + ($ID.id).split(',') #collate acceptable options of account ID's
			}
		}
		$AcceptableBatchValues = @($AcceptableBatchValues | where-object {$_ -ne ""} | Select-Object -Unique | Sort-Object) #Unique list of available batches that can be opened. @ converts this from a PSObject into an array which fixes the issue of -notin not working on PSobjects with only 1 item.
		ForEach ($BatchValue in $AcceptableBatchValues){
			if ($BatchValue.length -eq 2){
				$Script:TwoDigitBatchesUsed = $True
			}
		}
		do {
			if ($Null -ne $Batch -and $Batch -notin $AcceptableBatchValues){#if batch specified in the parameter isn't valid
				$Script:BatchToOpen = $Batch
				$Batch = $Null
				DisplayActiveAccounts
				Write-Host "`n Batch specified in Parameter is either incorrect or all accounts in that" -foregroundcolor Yellow
				Write-Host " batch are already open. Adjust your parameter or manually specify below.`n" -foregroundcolor Yellow
				start-sleep -milliseconds 5000
				exit
			}
			if ($Null -ne $Batch -and $Batch -in $AcceptableBatchValues){#if batch is valid, set variable so that loop can be exited.
				$Script:BatchToOpen = $Batch
			}
			ElseIf ($AcceptableBatchValues.count -eq 1){
				Write-Host " Opening Batch $X[38;2;255;165;000;22m$AcceptableBatchValues$X[0m.`n"
				$Script:BatchToOpen = $AcceptableBatchValues[0]
			}
			Else {
				Write-Host " Which Batch of accounts would you like to open (" -nonewline
				CommaSeparatedList $AcceptableBatchValues #write out each account option, comma separated but show each option in orange writing. Essentially output overly complicated fancy display options :)
				Write-Host ")?"
				if ($Null -eq $Batch){
					Write-Host " Alternatively, press '$X[38;2;255;165;000;22mc$X[0m' to cancel: " -nonewline
				}
				if ($Script:TwoDigitBatchesUsed -eq $True){
					$Script:BatchToOpen = ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 27 -TwoDigitAcctSelection $True #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). If no button is pressed, send "c" for cancel.
				}
				else {
					$Script:BatchToOpen = ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 27 #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). If no button is pressed, send "c" for cancel.
				}
				Write-Host
			}
			if ($Script:BatchToOpen -notin $AcceptableBatchValues + "c" + "Esc"){
				Write-Host " Invalid Input. Please enter one of the options above.`n" -foregroundcolor red
				$Script:BatchToOpen = ""
			}
		} until ($Script:BatchToOpen -in $AcceptableBatchValues + "c" + "Esc")
		if ($BatchToOpen -ne "c" -and $BatchToOpen -ne "Esc"){
			$Script:BatchedAccountIDsToOpen = New-Object -TypeName System.Collections.ArrayList
			ForEach ($ID in $Script:AccountOptionsCSV){
				if ($ID.id -in $Script:AcceptableBatchIDs -and $ID.batches.split(',') -contains $Script:BatchToOpen.tostring()){
					$Script:BatchedAccountIDsToOpen = $Script:BatchedAccountIDsToOpen + $ID.id
				}
			}
		}
		Else {
			$Script:OpenBatches = $False
		}
	}
	if ($Script:ParamsUsed -eq $false -and ($Script:RegionOption.length -ne 0 -or $Script:Region.length -ne 0)){
		$Script:Region = ""
		$Script:RegionOption = ""
	}
	if ($Script:BatchToOpen -ne "c" -and $Script:BatchToOpen -ne "Esc"){#get next region unless the cancel option has been specified.
		if ($Script:region.length -eq 0){#if no region parameter has been set already.
			ChooseRegion
		}
		Else {#if region parameter has been set already.
			if ($Script:region -eq "CN" -or $Script:region -eq "cn" -or $Script:region -eq 0){$Script:region = "cn.actual.battlenet.com.cn"}
			if ($Script:region -eq "NA" -or $Script:region -eq "us" -or $Script:region -eq 1){$Script:region = "us.actual.battle.net"}
			if ($Script:region -eq "EU" -or $Script:region -eq 2){$Script:region = "eu.actual.battle.net"}
			if ($Script:region -eq "Asia" -or $Script:region -eq "As" -or $Script:region -eq "KR" -or $Script:region -eq 3){$Script:region = "kr.actual.battle.net"}
			if ($Script:region -ne "us.actual.battle.net" -and $Script:region -ne "eu.actual.battle.net" -and $Script:region -ne "kr.actual.battle.net"){
				Write-Host " Region $region not valid. Please choose region" -foregroundcolor red
				ChooseRegion
			}
		}
	}
	$Script:AccountOptionsCSV = import-csv "$Script:WorkingDirectory\Accounts.csv" #Import accounts.csv again in case someone has updated Auth Method without closing the script.
	if ($Script:OpenAllAccounts -eq $True){
		Write-Host "`n Opening all accounts..."
		ForEach ($ID in $Script:AcceptableValues){
			$Script:AccountChoice = $Script:AccountOptionsCSV | where-object {$_.id -eq $ID}
			$Script:AccountID = $ID
			if ($id -eq $Script:AcceptableValues[-1]){
				$Script:LastAccount = $true
			}
			Write-Host "`n Opening Account: $ID"
			Processing
		}
		$Script:LastAccount = $False
		$Script:OpenAllAccounts = $False
		if ($Script:ParamsUsed -ne $True){
			Menu
		}
	}
	Else {
		if ($Script:OpenBatches -eq $True -and $Script:RegionOption -ne "c" -and $Script:RegionOption -ne "Esc"){
			if ($Script:BatchedAccountIDsToOpen.count -gt 1){
				$BatchPlural = "s"
			}
			Write-Host "`n Opening account$BatchPlural " -nonewline
			CommaSeparatedList $Script:BatchedAccountIDsToOpen -AndText
			Write-Host " from batch $BatchToOpen..."
			ForEach ($ID in $Script:BatchedAccountIDsToOpen){
				$Script:AccountChoice = $Script:AccountOptionsCSV | where-object {$_.id -eq $ID}
				$Script:AccountID = $ID
				if ($id -eq $Script:BatchedAccountIDsToOpen[-1]){
					$Script:LastAccount = $true
				}
				Write-Host "`n Opening Account: $ID..."
				Processing
			}
			$Script:LastAccount = $False
			$Script:OpenBatches = $False
			Start-Sleep -milliseconds 500
			if ($Script:ParamsUsed -ne $True){
				Menu
			}
		}
		Else {
			if ($Script:BatchToOpen -ne "c" -and $Script:BatchToOpen -ne "Esc"){
				Processing
			}
			if ($Script:ParamsUsed -ne $true){
				Menu
			}
			else {
				Write-Host "I'm quitting LOL"
				exit
			}
		}
	}
}
Function ChooseAccount {
	if ($Null -eq $Script:AccountUsername){#if no account parameters have been set already
		do {
			if ($Script:AccountID -eq "t"){
				TerrorZone
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "d"){
				DClone -DisableOutput $False -DCloneTrackerSource $Script:Config.DCloneTrackerSource -TagList $Script:Config.DCloneAlarmList -EmuToken $Script:EmuToken
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "j"){
				JokeMaster
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "i"){
				Inventory #show stats
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "o"){ #options menu
				Options
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "s"){
				if ($Script:AskForSettings -eq $True){
					Write-Host "  Manual Setting Switcher Disabled." -foregroundcolor Green
					$Script:AskForSettings = $False
				}
				else {
					Write-Host "  Manual Setting Switcher Enabled." -foregroundcolor Green
					$Script:AskForSettings = $True
				}
				Start-Sleep -milliseconds 1550
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "g"){#silly thing to replicate in game chat gem.
				$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv"
				if ($Script:GemActivated -ne $True){
					$GibberingGemstone = get-random -minimum 0 -maximum  4095
					if ($GibberingGemstone -eq 69 -or $GibberingGemstone -eq 420){#nice
						Write-Host "  Perfect Gem Activated" -ForegroundColor magenta
						Write-Host "`n     OMG!" -foregroundcolor green
						$Script:PGemActivated = $True
						([int]$Script:CurrentStats.PerfectGems) ++
						SetQualityRolls
						Start-Sleep -milliseconds 4567
					}
					else {
						if ($GibberingGemstone -in 16..32){
							CowKingKilled
							$SkipCSVExport = $True
						}
						else {
							Write-Host "  Gem Activated" -ForegroundColor magenta
							([int]$Script:CurrentStats.Gems) ++
						}
					}
					$Script:GemActivated = $True
					SetQualityRolls
					if ($SkipCSVExport -ne $True){
						try {
							$CurrentStats | Export-Csv -Path "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation #update Stats.csv with Gems/PGems.
						}
						Catch {
							Write-Host "  Couldn't update stats.csv" -foregroundcolor yellow
						}
					}
				}
				Else {
					Write-Host "  Gem Deactivated" -ForegroundColor magenta
					$Script:GemActivated = $False
					SetQualityRolls
				}
				Start-Sleep -milliseconds 850
				$Script:AccountID = "r"
			}
			if ($Script:AccountID -eq "r"){#refresh
				Clear-Host
				if ($Script:ScriptHasBeenRun -eq $true){
					if ($DebugMode -eq $true){
						DisplayPreviousAccountOpened
					}
				}
				Notifications -check $True
				BannerLogo
				QuoteRoll
			}
			if ($Script:AccountID -eq "e"){
				Start-Process -FilePath "$Script:WorkingDirectory\CSVpad\CSVpad.exe" -ArgumentList "$Script:WorkingDirectory\Accounts.csv"
				Start-Sleep -milliseconds 500
				$Script:AccountID = "r"
			}
			CheckActiveAccounts
			if ($null -ne $Close){
				if ($Close -eq "all"){
					stop-process -name d2r #Indescriminately KILL ALL D2r processes muahahaha.
					Write-Host "All D2r instances closed."
					Exit
				}
				else {
					$ProcessToKill = ($Script:ActiveAccountsList | where-object {$_.ID -eq $close}).ProcessID
					try {
						stop-process -id $ProcessToKill
						Write-Host "Process killed for account $Close."
					}
					Catch {
						write-host "Account $close isn't currently running."
					}
					Exit
				}
			}
			DisplayActiveAccounts	
			$AdditionalTimeSpan = New-TimeSpan -Start $Script:StartTime -End (Get-Date) #work out elapsed time for session timer and if time tracking enabled for adding time to accounts.csv
			if ($Script:ActiveAccountsList.id.length -ne 0){#if there are active accounts open, add to total session time
				if ($Script:IdleLimitForAccountUseTime -ne "" -and $Script:IdleLimitForAccountUseTime -gt 0){#If Idlelimitfeature is used, calc idle time and check if we should append session time.
					$Script:IdleMinutes = [int](IdleTimeChecker)/60
					if ($IdleMinutes -lt $Script:IdleLimitForAccountUseTime){ #If we've been idle less than what's specified in config.xml, append time
						$Script:SessionTimer = $Script:SessionTimer + $AdditionalTimeSpan #track current session time but only if a game is running
					}
				}
				Else {
					$Script:SessionTimer = $Script:SessionTimer + $AdditionalTimeSpan #track current session time but only if a game is running
				}
			}
			if ($Script:Config.TrackAccountUseTime -eq $True){
				$OpenD2LoaderInstances = Get-WmiObject -Class Win32_Process | Where-Object { $_.name -eq "powershell.exe" -and $_.commandline -match $Script:ScriptFileName} | Select-Object name,processid,creationdate | Sort-Object creationdate -descending
				if ($OpenD2LoaderInstances.length -gt 1){#If there's more than 1 D2loader.ps1 script open, close until there's only 1 open to prevent the time played accumulating too quickly.
					ForEach ($Process in $OpenD2LoaderInstances[1..($OpenD2LoaderInstances.count -1)]){
						Stop-Process -id $Process.processid -force #Closes oldest running d2loader script
					}
				}
				if ($Script:ActiveAccountsList.id.length -ne 0){#if there are active accounts open add to total script time
					if ($Script:IdleLimitForAccountUseTime -ne "" -and $Script:IdleLimitForAccountUseTime -gt 0){#If Idlelimitfeature is used, calc idle time and check if we should skip adding time.
						$Skip = $False
						$Script:IdleMinutes = [int](IdleTimeChecker)/60
						if ($IdleMinutes -gt $Script:IdleLimitForAccountUseTime){#If we've been idle more than what's specified in config.xml, skip append time
							$Skip = $True
						}
					}
					if ($Skip -ne $True){#If computer isn't idle OR if we're not bothering with checking idle status.
						#Add time for each account that's open
						$Script:AccountOptionsCSV = import-csv "$Script:WorkingDirectory\Accounts.csv"
						ForEach ($AccountID in $Script:ActiveAccountsList.id |Sort-Object){ #$Script:ActiveAccountsList.id
							$AccountToUpdate = $Script:AccountOptionsCSV | Where-Object {$_.ID -eq $accountID}
							if ($AccountToUpdate){
								$AdditionalTimeSpan = New-TimeSpan -Start $Script:StartTime -End (Get-Date)
								try {#get current time from csv and add to it
									$AccountToUpdate.TimeActive = [TimeSpan]::Parse($AccountToUpdate.TimeActive) + $AdditionalTimeSpan
								}
								Catch {#if CSV hasn't been populated with a time yet.
									$AccountToUpdate.TimeActive = $AdditionalTimeSpan
								}
							}
							try {
								$Script:AccountOptionsCSV | Export-Csv -Path "$Script:WorkingDirectory\Accounts.csv" -NoTypeInformation #update accounts.csv with the new time played.
							}
							Catch {
								$WriteAcctCSVError = $True
							}
						}
						if ($WriteAcctCSVError -eq $true){
							Write-Host "`n  Couldn't update accounts.csv with playtime info." -ForegroundColor Red
							Write-Host "  It's likely locked for editing, please ensure you close this file." -ForegroundColor Red
							start-sleep -milliseconds 1500
							$WriteAcctCSVError = $False
						}
						#Add Time to Total Script Time only if there's an open game.
						$Script:CurrentStats = import-csv "$Script:WorkingDirectory\Stats.csv"
						try {
							$AdditionalTimeSpan = New-TimeSpan -Start $Script:StartTime -End (Get-Date)
							try {#get current time from csv and add to it
								$Script:CurrentStats.TotalGameTime = [TimeSpan]::Parse($CurrentStats.TotalGameTime) + $AdditionalTimeSpan
							}
							Catch {#if CSV hasn't been populated with a time yet.
								$Script:CurrentStats.TotalGameTime = $AdditionalTimeSpan
							}
							$CurrentStats | Export-Csv -Path "$Script:WorkingDirectory\Stats.csv" -NoTypeInformation #update Stats.csv with Total Time played.
						}
						Catch {
							Write-Host "`n  Couldn't update Stats.csv with playtime info." -ForegroundColor Red
							Write-Host "  It's likely locked for editing, please ensure you close this file." -ForegroundColor Red
							start-sleep -milliseconds 1500
						}
						Write-Debug "`n  Added time to CSV: $($AdditionalTimeSpan.minutes).$($AdditionalTimeSpan.seconds).$($AdditionalTimeSpan.milliseconds)"
					}
				}
			}
			$Script:StartTime = Get-Date #restart timer for session time (and account time if user is tracking that too).
			$Script:AcceptableValues = New-Object -TypeName System.Collections.ArrayList
			$Script:TwoDigitIDsUsed = $False
			ForEach ($AccountOption in $Script:AccountOptionsCSV){
				if ($AccountOption.id -notin $Script:ActiveAccountsList.id){
					$Script:AcceptableValues = $AcceptableValues + ($AccountOption.id) #+ "x"
					if ($AccountOption.id.length -eq 2){
						$Script:TwoDigitIDsUsed = $True
					}
				}
			}
			$accountoptions = ($Script:AcceptableValues -join  ", ").trim()
			#DClone Alarm check
			if (!($Script:EmuOfflineMode -eq $True -and $Script:Config.DCloneTrackerSource -eq "d2emu.com")){ #if D2Emu is down and connection source is D2Emu, disable this feature by skipping it.
				write-debug "Checking for DClone status changes."
				$GetDCloneFunc = $(Get-Command DClone).Definition
				$GetWebRequestFunc = $(Get-Command WebRequestWithTimeOut).Definition
				if ($Script:Config.DCloneAlarmList -ne ""){ # If DClone alarms should be checked on refresh
					try {
						if ($null -ne $Script:DCloneChangesCSV){
							$Script:DCloneChangesCSV = Receive-Job $Script:DCloneJob
							if ($DebugMode -eq $True){
								formatfunction -indents 1 -iswarning -text $Script:DCloneChangesCSV #debugging
							}
							if ($Script:DCloneChangesCSV -match "true"){#if any of the text contains True
								DCloneVoiceAlarm #Create Voice Alarm
							}
						}
						Else {
							$Script:DCloneChangesCSV = "" # If menu is refreshed too quick
						}
						if ($null -ne $RunOnce){ # Don't try remove job on startup for faster launch.
							Get-Job | Where-Object { $Script:JobIDs -notcontains $_.Id } | Remove-Job -Force
						}
						$Script:DCloneJob = Start-Job -ScriptBlock {
							Invoke-Expression "function Dclone {$using:GetDCloneFunc}"
							Invoke-Expression "function WebRequestWithTimeOut {$Using:GetWebRequestFunc}"
							DClone -DisableOutput $True -DCloneTrackerSource $Using:Config.DCloneTrackerSource -TagList $Using:Config.DCloneAlarmList -DCloneChanges $using:DCloneChangesCSV -DCloneAlarmLevel $Using:DCloneAlarmLevel -EmuToken $Using:EmuToken
						} #check for dclone status
					}
					catch {
						Write-Host "`n Unable to check for DClone status via $($Script:Config.DCloneTrackerSource)." -Foregroundcolor Red
						Write-Host " Try restarting script or changing the source in config.xml." -Foregroundcolor Red
					}
				}
			}
			else {
				write-debug "Not checking for DClone status changes."
			}
			if ($Script:MovedWindowLocations -ge 1){ # Tidy up any old jobs created for moving windows.
				Get-Job | Where-Object { $Script:JobIDs -contains $_.Id -and $_.state -ne "Running"} | Remove-Job -Force
				$Script:JobIDs = @()
				$Script:MovedWindowLocations = 0
			}
			do {
				Write-Host
				$Script:OpenAllAccounts = $False
				if ($accountoptions.length -gt 0){#if there are unopened account options available
					if ($Script:Config.DisableOpenAllAccountsOption -eq $true){#if end user has a stupid amount of accounts and wants to prevent accidentally opening up over 9000 accounts
						$AllAccountMenuText = ""
						$AllAccountMenuTextNoBatch = ""
					}
					else {
						$AllOption = "a"
						$AllAccountMenuText = "'$X[38;2;255;165;000;22ma$X[0m' to open All accounts, "
						$AllAccountMenuTextNoBatch = " 或者 按 【$X[38;2;255;165;000;22ma$X[0m】 打开全部."
					}
					if ($Script:EnableBatchFeature -ne $true){
						$BatchMenuText = ""
						if ($accountoptions.length -le 24){ #if so many accounts are available to be used that it's too long and impractical to display all the individual options.
							Write-Host ("  选择要打开的账号: " + "$X[38;2;255;165;000;22m$accountoptions$X[0m" + $AllAccountMenuTextNoBatch)
							Write-Host ""
							Write-Host "  也可以选择以下菜单选项:"
						}
						Else {
							Write-Host " 输入要登录的账号 ID#."
							Write-Host " 也可以选择以下菜单选项:"
							Write-Host ("  " + $AllAccountMenuText)
						}
					}
					else {
						$Script:BatchToOpen = $Null
						$BatchMenuText = "'$X[38;2;255;165;000;22mb$X[0m' 批量打开账号,"
						$Script:AcceptableBatchIDs = $Null #reset value
						$AcceptableBatchValues = $Null
						ForEach ($ID in $Script:AccountOptionsCSV){
							if ($ID.id -in $Script:AcceptableValues){#Find batch values to choose from based on accounts that aren't already open.
								$AcceptableBatchValues = $AcceptableBatchValues + ($ID.batches).split(',')
								$Script:AcceptableBatchIDs = $Script:AcceptableBatchIDs + ($ID.id).split(',')
							}
						}
						$AcceptableBatchValues = @($AcceptableBatchValues | where-object {$_ -ne ""} | Select-Object -Unique | Sort-Object) #Unique list of available batches that can be opened
						if ($Null -eq $AcceptableBatchValues[0]){
							$Script:BatchOption = ""
							$BatchMenuText = ""
							if ($accountoptions.length -le 24){ #if so many accounts are available to be used that it's too long and impractical to display all the individual options.
								Write-Host ("  选择要登录的账号: " + "$X[38;2;255;165;000;22m$accountoptions$X[0m" + $AllAccountMenuTextNoBatch)
								Write-Host "  也可以选择以下菜单选项:"
							}
							Else {
								Write-Host " 输入要登录的账号 ID#."
								Write-Host " 也可以选择以下菜单选项:"
								Write-Host ("  " + $AllAccountMenuText + $BatchMenuText)
							}
						}
						Else {
							$BatchOption = "b"
							Write-Host " 输入要登录的账号 ID#."
							Write-Host " 也可以选择以下菜单选项:"
							Write-Host ("  " + $AllAccountMenuText + $BatchMenuText)
						}
					}
				}
				Else {#if there aren't any available options, IE all accounts are open
					$AllOption = $Null
					$BatchOption = $Null
					Write-Host " 所有账号都已打开!" -foregroundcolor yellow
				}
				$DCloneOption = "d"
				$TerrorZoneOption = "t"
                # 仅排版调整：四行布局，保留原有条件显示逻辑与颜色
                $ShowT = $True
                if ($Script:EmuOfflineMode -eq $True){
                    $ShowT = $False
                    $TerrorZoneOption = ""
                    if ($Script:Config.DCloneTrackerSource -eq "d2emu.com"){ $DCloneOption = "" }
                }
                # 第1行: r 刷新    d 黑毛信息  t 恐怖地带
                $line1 = "  【$X[38;2;255;165;000;22mr$X[0m】刷新"
                if ($DCloneOption -eq "d"){ $line1 += "   【$X[38;2;255;165;000;22md$X[0m】黑毛信息" }
                if ($ShowT){ $line1 += "  【$X[38;2;255;165;000;22mt$X[0m】 恐怖地带" }
                Write-Host $line1
                # 第2行: o 选项   e 编辑账号
                Write-Host ("  【$X[38;2;255;165;000;22mo$X[0m】选项   【$X[38;2;255;165;000;22me$X[0m】编辑账号")
                # 第3行: s 手动切换设置 (仅在启用时显示)
                if ($Script:Config.ManualSettingSwitcherEnabled -eq $true){
                    $ManualSettingSwitcherOption = "s"
                    Write-Host ("  【$X[38;2;255;165;000;22ms$X[0m】手动切换设置")
                }
                else {
                    $ManualSettingSwitcherOption = $null
                }
                # 第4行: i 信息   x 退出 (保持末尾无换行等待输入)
                Write-Host ("  【$X[38;2;255;165;000;22mi$X[0m】信息   【$X[38;2;255;165;000;22mx$X[0m】" + "$X[38;2;255;000;000;22m退出$X[0m: ") -NoNewLine
				$EditOption = "e"
				if ($Script:TwoDigitIDsUsed -eq $True){
					$Script:AccountID = ReadKeyTimeout "" $MenuRefreshRate "r" -TwoDigitAcctSelection $True #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). if no button is pressed, send "r" for refresh.
				}
				else {
					$Script:AccountID = ReadKeyTimeout "" $MenuRefreshRate "r" #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). if no button is pressed, send "r" for refresh.
				}
				if ($Script:AccountID -notin ($Script:AcceptableValues + "x" + "r" + "g" + "j" + "i" + "o" + $TerrorZoneOption + $DCloneOption + $ManualSettingSwitcherOption + $AllOption + $BatchOption + $EditOption) -and $Null -ne $Script:AccountID){
					if ($Script:AccountID -eq "a" -and $Script:Config.DisableOpenAllAccountsOption -ne $true){
						Write-Host " 无法打开所有账号, 因为所有账号都已打开!" -foregroundcolor red
					}
					else {
						Write-Host " 无效输入. 请输入以上选项之一." -foregroundcolor red
					}
					$Script:AccountID = $Null
				}
			} until ($Null -ne $Script:AccountID)
			if ($Null -ne $Script:AccountID){
				if ($Script:AccountID -eq "x"){
					Write-Host "`n 祝你有美好的一天 :)" -foregroundcolor yellow
					Start-Sleep -milliseconds 486
					Exit
				}
				$Script:AccountChoice = $Script:AccountOptionsCSV | where-object {$_.id -eq $Script:AccountID} #filter out to only include the account we selected.
			}
			$Script:RunOnce = $True
		} until ($Script:AccountID -ne "r" -and $Script:AccountID -ne "t" -and $Script:AccountID -ne "d" -and $Script:AccountID -ne "g" -and $Script:AccountID -ne "j" -and $Script:AccountID -ne "s" -and $Script:AccountID -ne "i" -and $Script:AccountID -ne "o" -and $Script:AccountID -ne "e")
		if ($Script:AccountID -eq "a"){
			$Script:OpenAllAccounts = $True
		}
		if ($Script:AccountID -eq "b"){
			$Script:OpenBatches = $True
		}
	}
	if (($Null -ne $Script:AccountUsername -and ($Null -eq $Script:PW -or "" -eq $Script:PW) -or ($Script:AccountChoice.id.length -gt 0 -and $Script:AccountChoice.PW.length -eq 0))){#This is called when params are used but the password wasn't entered. Not used for -all or -batch
		if ($Null -ne $Script:AccountOptionsCSV){#compare parameter against account ID in case they specified ID instead of email.
			$Script:AccountChoice = $Script:AccountOptionsCSV | where-object {$_.id -eq $Script:AccountUsername}
			if ($Null -eq $Script:AccountChoice){#if still null, compare against username in accounts.csv
				$Script:AccountChoice = $Script:AccountOptionsCSV | where-object {$_.acct -eq $Script:AccountUsername}
			}
			if ($Null -ne $Script:AccountChoice){
				$Script:AccountID = $Script:AccountChoice.id
				$Script:PWmanualset = $False
				$Script:ParamLaunchAndAccountNotInAccountsCSV = $False #used in processing to determine if some features should be skipped.
				return
			}
			else {
				$Script:ParamLaunchAndAccountNotInAccountsCSV = $True #Is this var name too long? No? Cool.
			}
		}
		$SecuredPW = read-host -AsSecureString " 输入账号 $Script:AccountUsername 的 Battle.net 密码"
		$Bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecuredPW)
		$Script:PW = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($Bstr)
		$Script:AccountOptionsCSV = @(
			[pscustomobject]@{PW=$Script:PW;acct=$Script:AccountUsername}
		)
		$Script:PWmanualset = $true
	}
	else {
		$Script:PWmanualset = $false
	}
}
Function ChooseRegion {#AKA Realm. Not to be confused with the actual Diablo servers that host your games, which are all over the world :)
	Write-Host " 以下是可用区服:"
	Write-Host "   选项    区服     服务器地址"
	Write-Host "  ------  ------  --------------"
	ForEach ($Server in $ServerOptions){
		if ($Server.region.length -eq 2){$Regiontablespacing = " "}
		if ($Server.region.length -eq 4){$Regiontablespacing = ""}
		Write-Host ("   【" + $Server.option + "】  " + $Regiontablespacing + $RegionCNMap[$Server.region] + $Regiontablespacing + "   " + $Server.region_server) -foregroundcolor green
	}
	do {
		Write-Host "`n 按数字选择区服: $X[38;2;255;165;000;22m0$X[0m, $X[38;2;255;165;000;22m1$X[0m, $X[38;2;255;165;000;22m2$X[0m 或 $X[38;2;255;165;000;22m3$X[0m"
		if ($Script:Config.DefaultRegion -ne ""){
			$DefaultRegionText = " 或按 【$X[38;2;255;165;000;22m回车$X[0m】 直接使用默认区服 (" + $Config.DefaultRegion + "-" + ($Script:ServerOptions | Where-Object {$_.option -eq $Config.DefaultRegion}).region + ")"
		}
		Write-Host (" 也可以按 【$X[38;2;255;165;000;22mc$X[0m】 取消" + $DefaultRegionText + ": ") -nonewline
		$Script:RegionOption = ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 13,27 #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). If no button is pressed, send "c" for cancel.
		if ("" -eq $Script:RegionOption){
			$Script:RegionOption = $Config.DefaultRegion #default to NA
		}
		else {
			$Script:RegionOption = $Script:RegionOption.tostring()
		}
		if ($Script:RegionOption -notin $Script:ServerOptions.option + "c" + "Esc"){
			Write-Host " 无效输入. 请输入以上选项之一." -foregroundcolor red
			$Script:RegionOption = ""
		}
	} until ("" -ne $Script:RegionOption)
	if ($Script:RegionOption -in 0..3 ){# if value is 0,1,2 or 3 set the region string.
		$Script:region = ($ServerOptions | where-object {$_.option -eq $Script:RegionOption}).region_server
		$Script:RegionLabel = $Script:Region.substring(0,2)
		if ($Script:RegionLabel -eq "US"){$Script:RegionLabel = "NA"}
		$Script:LastRegion = $Script:Region
	}
}
Function Processing {
	if ($Script:RegionOption -ne "c" -and $Script:RegionOption -ne "Esc"){
		if (($Script:PW -eq "" -or $Null -eq $Script:PW) -and $Script:PWmanualset -eq 0){
			$Script:PW = $Script:AccountChoice.PW.tostring()
		}
		if ($Script:AccountChoice.AuthenticationMethod -ne "Token"){
			try {
				if ($Script:ParamLaunchAndAccountNotInAccountsCSV -eq $False -or $Script:ParamsUsed -ne $true -or ($Script:ParamsUsed -eq $true -and ($Script:OpenBatches -eq $True -or $Script:OpenAllAccounts -eq $True))){ # If Params aren't used or if Params are used with either batches or open all accounts.
					$Script:acct = $Script:AccountChoice.acct.tostring()
					if ($Config.ConvertPlainTextSecrets -eq $true -or $PW.Length -gt 200){ # if PW should be converted, update $Script:PW to the converted PW, otherwise leave the variable alone.
						$EncryptedPassword = $PW | ConvertTo-SecureString -ErrorAction Stop -Errorvariable ErrorVar #Try converting password.
						$PWobject = New-Object System.Management.Automation.PsCredential("N/A", $EncryptedPassword)
						$Script:PW = $PWobject.GetNetworkCredential().Password
					}
				}
				else {
					if ($Null -eq $Script:AccountID){
						$Script:acct = $Script:AccountUsername
						$Script:AccountID = "1"
					}
				}
			}
			Catch {
				if ($ErrorVar -match "不是有效的加密字符串"){
					Write-Host " 解密密码时出现问题." -foregroundcolor red
					FormatFunction -Text ("请重新输入账号 " + $Script:AccountChoice.acct + " 的密码到 accounts.csv 并重新运行脚本.`n") -IsError
					PressTheAnyKey
				}
				else {
					Write-Host " 账号密码在 accounts.csv 中是明文." -foregroundcolor red
					Write-Host " 重新运行脚本并你的密码将被加密." -foregroundcolor red
					Write-Host " If errors persist, re-enter your password in accounts.csv`n" -foregroundcolor red
					PressTheAnyKey
				}
			}
		}
		if ($ParamsUsed -eq $True){
			$Script:RegionLabel = $Script:Region.substring(0,2)
			if ($Script:RegionLabel -eq "US"){$Script:RegionLabel = "NA"}
		}
		if ($Script:AccountChoice.AuthenticationMethod -eq "Token" -or (($Script:ForceAuthToken -eq $True -or $Script:Config.ForceAuthTokenForRegion -match $RegionLabel) -and $Script:ParamLaunchAndAccountNotInAccountsCSV -ne $True)){
			if ($Script:AccountChoice.Token.length -gt 200){
				$Script:Token = $Script:AccountChoice.Token.tostring()
				$EncryptedToken = $Script:Token | ConvertTo-SecureString
				$Tokenobject = New-Object System.Management.Automation.PsCredential("N/A", $EncryptedToken)
				$Token = $Tokenobject.GetNetworkCredential().Password
			}
			Else {# if token isn't stored as a secure string due to ConvertPlainTextSecrets being set to false.
				$Token = $Script:AccountChoice.Token
			}
			$Entropy = @(0xc8, 0x76, 0xf4, 0xae, 0x4c, 0x95, 0x2e, 0xfe, 0xf2, 0xfa, 0x0f, 0x54, 0x19, 0xc0, 0x9c, 0x43)
			# Convert the token and entropy to byte arrays
			$TokenBytes = [System.Text.Encoding]::UTF8.GetBytes($Token)
			$EntropyBytes = [byte[]] $Entropy
			# Encrypt the token
			[void][System.Reflection.Assembly]::LoadWithPartialName("System.Security")
			$ProtectedData = [System.Security.Cryptography.ProtectedData]::Protect($TokenBytes, $EntropyBytes, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
			$Path = "HKCU:\SOFTWARE\Blizzard Entertainment\Battle.net\Launch Options\OSI"
			Set-ItemProperty -Path $Path -Name "REGION" -Value $Script:Region.Substring(0, 2).ToUpper()
			Set-ItemProperty -Path $Path -Name "WEB_TOKEN" -Value $ProtectedData -Type Binary
		}
		try {
			$Script:AccountFriendlyName = $Script:AccountChoice.accountlabel.tostring()
		}
		Catch {
			$Script:AccountFriendlyName = $Script:AccountUsername
		}
		#Open diablo with parameters
			# IE, this is essentially just opening D2r like you would with a shortcut target of "C:\Program Files (x86)\Battle.net\Games\Diablo II Resurrected\D2R.exe" -username <yourusername -password <yourPW> -address <SERVERaddress>
		if ($Script:ParamLaunchAndAccountNotInAccountsCSV -ne $True){
			$CustomLaunchArguments = ($Script:AccountChoice.CustomLaunchArguments).replace("`"","").replace("'","") #clean up arguments in case they contain quotes (for folks that have used excel to edit accounts.csv).
		}
		if ($Script:AccountChoice.AuthenticationMethod -eq "Parameter" -and $Script:ForceAuthToken -ne $True -and $Script:Config.ForceAuthTokenForRegion -notmatch $RegionLabel -or $Script:ParamLaunchAndAccountNotInAccountsCSV -eq $True){
			$arguments = (" -username " + $Script:acct + " -password " + $Script:PW + " -address " + $Script:Region + " " + $CustomLaunchArguments).tostring()
		}
		else {
			$arguments = (" -uid osi " + $CustomLaunchArguments).tostring()
		}
		if ($Config.ForceWindowedMode -eq $true){#starting with forced window mode sucks, but someone asked for it.
			$arguments = $arguments + " -w"
		}
		$Script:PW = $Null
		$Script:Token = $Null
		# 基于区服动态选择 客户端路径 和 Settings 存档路径（CN 使用 "Diablo II Resurrected (CN)"）
		$isCN = $false
		try { if ($Script:RegionLabel -eq "CN") { $isCN = $true } } catch {}
		if (-not $isCN) { try { if ($Script:Region -match '(?i)cn|china') { $isCN = $true } } catch {} }
		$basePath = if ($isCN) { $Script:SettingsProfilePathCN } else { $Script:SettingsProfilePathGlobal }
		if ([string]::IsNullOrWhiteSpace($basePath)) { $basePath = $Script:SettingsProfilePath }
		if (-not (Test-Path $basePath)) { try { New-Item -ItemType Directory -Path $basePath -Force | Out-Null } catch {} }
		$SettingsProfilePath = $basePath
		#Switch Settings file to load D2r from.
		if ($Config.SettingSwitcherEnabled -eq $True -or $Script:AskForSettings -eq $True){
			if ($Script:AccountChoice.CustomLaunchArguments -match "-mod"){
				$pattern = "-mod\s+(\S+)" #pattern to find the first word after -mod
				if ($Script:AccountChoice.CustomLaunchArguments -match $pattern){
					$ModName = $matches[1]
					try {
						Write-Verbose "尝试获取Mod信息..."
						try {
							$__GamePathForMod = if ($isCN -and $Script:GamePathCN) { $Script:GamePathCN } else { $Script:GamePath }
							$Modinfo = ((Get-Content "$($__GamePathForMod)\Mods\$ModName\$ModName.mpq\Modinfo.json" -ErrorAction silentlycontinue | ConvertFrom-Json).savepath).Trim("/")
						}
						catch {
							try {
								$__GamePathForMod = if ($isCN -and $Script:GamePathCN) { $Script:GamePathCN } else { $Script:GamePath }
								$Modinfo = ((Get-Content "$($__GamePathForMod)\Mods\$ModName\Modinfo.json" -ErrorAction stop -ErrorVariable ModReadError | ConvertFrom-Json).savepath).Trim("/")
							}
							catch {
								$__GamePathForMod = if ($isCN -and $Script:GamePathCN) { $Script:GamePathCN } else { $Script:GamePath }
								FormatFunction -Text "使用标准设置路径. 找不到 Modinfo.json 在 '$(__GamePathForMod)\Mods\$ModName\$ModName.mpq'" -IsWarning
								start-sleep -milliseconds 1500
							}
						}
						If ($Null -eq $Modinfo){
							Write-Verbose " 没有为这个mod指定自定义保存路径."
						}
						ElseIf ($Modinfo -ne "../"){
							$SettingsProfilePath = $SettingsProfilePath + "mods\$Modinfo\"
							if (-not (Test-Path $SettingsProfilePath)){
								Write-Host " Mod 保存文件夹不存在. 创建文件夹..."
								New-Item -ItemType Directory -Path $SettingsProfilePath -ErrorAction stop | Out-Null
								Write-Host " Created folder: $SettingsProfilePath" -ForegroundColor Green
							}
							Write-Host " Mod: $ModName 检测到. 使用自定义路径 for settings.json." -ForegroundColor Green
							Write-Verbose " $SettingsProfilePath"
						}
						Else {
							Write-Verbose " Mod 已使用但保存路径是标准."
						}
					}
					Catch {
						Write-Verbose " Mod 已使用但自定义保存路径未指定."
					}
				}
				else {
					Write-Host " 无法检测 Mod 名称. 标准路径用于 settings.json." -ForegroundColor Red
				}
			}	
		}		
		if ($Config.SettingSwitcherEnabled -eq $True -and $Script:AskForSettings -ne $True -and $Script:ParamLaunchAndAccountNotInAccountsCSV -ne $True){#if user has enabled the auto settings switcher.
			$SettingsJSON = ($SettingsProfilePath + "Settings.json")
			if ((Test-Path -Path ($SettingsProfilePath + "Settings.json")) -eq $true){ #check if settings.json does exist in the savegame path (if it doesn't, this indicates first time launch or use of a new single player mod).
				ForEach ($id in $Script:AccountOptionsCSV){#create a copy of settings.json file per account so user doesn't have to do it themselves
					if ((Test-Path -Path ($SettingsProfilePath + "Settings" + $id.id + ".json")) -ne $true){#if somehow settings<ID>.json doesn't exist yet make one from the current settings.json file.
						try {
							Copy-Item $SettingsJSON ($SettingsProfilePath + "Settings" + $id.id + ".json") -ErrorAction Stop
						}
						catch {
							FormatFunction -Text "`n找不到 settings.json 在 $SettingsProfilePath" -IsError
							if ($Script:AccountChoice.CustomLaunchArguments -match "-mod"){
								Break
							}
							Else {
								Write-Host " 正常启动游戏 (通过 战网客户端) & 这个文件将被重建." -foregroundcolor red
							}
							Write-Host
							PressTheAnyKeyToExit
						}
					}
				}
				try {
					Copy-item ($SettingsProfilePath + "settings" + $Script:AccountID + ".json") $SettingsJSON -ErrorAction Stop #overwrite settings.json with settings<ID>.json (<ID> being the account ID). This means any changes to settings in settings.json will be lost the next time an account is loaded by the script.
					$CurrentLabel = ($Script:AccountOptionsCSV | where-object {$_.id -eq $Script:AccountID}).accountlabel
					formatfunction -text ("自定义游戏设置 (settings" + $Script:AccountID + ".json) 用于 " + $CurrentLabel) -success
					Start-Sleep -milliseconds 133
				}
				catch {
					FormatFunction -Text "无法覆盖 settings.json 因为某些原因. 确保你没有打开文件!" -IsError
					PressTheAnyKey
				}
			}
		}
		if ($Script:AskForSettings -eq $True){#steps go through if user has toggled on the manual setting switcher ('s' in the menu).
			$SettingsJSON = ($SettingsProfilePath + "Settings.json")
			$files = Get-ChildItem -Path $SettingsProfilePath -Filter "settings.*.json"
			$Counter = 1
			if ((Test-Path -Path ($SettingsProfilePath+ "Settings" + $id.id + ".json")) -ne $true){#if somehow settings<ID>.json doesn't exist yet make one from the current settings.json file.
				try {
					Copy-Item $SettingsJSON ($SettingsProfilePath + "Settings" + $id.id + ".json") -ErrorAction Stop
				}
				catch {
					Write-Host "`n 找不到 settings.json 在 $SettingsProfilePath" -foregroundcolor red
					Write-Host " 请正常启动游戏 (通过 战网客户端) & 这个文件将被重建." -foregroundcolor red
					PressTheAnyKeyToExit
				}
			}
			$SettingsDefaultOptionArray = New-Object -TypeName System.Collections.ArrayList #Add in an option for the default settings file (if it exists, if the auto switcher has never been used it won't appear.
			$SettingsDefaultOption = New-Object -TypeName psobject
			$SettingsDefaultOption | Add-Member -MemberType NoteProperty -Name "ID" -Value $Counter
			$SettingsDefaultOption | Add-Member -MemberType NoteProperty -Name "Name" -Value ("Default - settings" + $Script:AccountID + ".json")
			$SettingsDefaultOption | Add-Member -MemberType NoteProperty -Name "FileName" -Value ("settings" + $Script:AccountID + ".json")
			[VOID]$SettingsDefaultOptionArray.Add($SettingsDefaultOption)
			$SettingsFileOptions = New-Object -TypeName System.Collections.ArrayList
			ForEach ($file in $files){
				 $SettingsFileOption = New-Object -TypeName psobject
				 $Counter = $Counter + 1
				 $Name = $file.Name -replace '^settings\.|\.json$' #remove 'settings.' and '.json'. The text in between the two periods is the name.
				 $SettingsFileOption | Add-Member -MemberType NoteProperty -Name "ID" -Value $Counter
				 $SettingsFileOption | Add-Member -MemberType NoteProperty -Name "Name" -Value $Name
				 $SettingsFileOption | Add-Member -MemberType NoteProperty -Name "FileName" -Value $file.Name
				 [VOID]$SettingsFileOptions.Add($SettingsFileOption)
			}
			if ($Null -ne $SettingsFileOptions){# If settings files are found, IE the end user has set them up prior to running script.
				$SettingsFileOptions = $SettingsDefaultOptionArray + $SettingsFileOptions
				Write-Host "  设置选项你可以选择的是:"
				ForEach ($Option in $SettingsFileOptions){
					Write-Host ("   " + $Option.ID + ". " + $Option.name) -foregroundcolor green
				}
				do {
					Write-Host "  选择你喜欢的设置文件: " -nonewline
					ForEach ($Value in $SettingsFileOptions.ID){ #write out each account option, comma separated but show each option in orange writing. Essentially output overly complicated fancy display options :)
						if ($Value -ne $SettingsFileOptions.ID[-1]){
							Write-Host "$X[38;2;255;165;000;22m$Value$X[0m" -nonewline
							if ($Value -ne $SettingsFileOptions.ID[-2]){Write-Host ", " -nonewline}
						}
						else {
							Write-Host " or $X[38;2;255;165;000;22m$Value$X[0m"
						}
					}
					if ($Null -eq $ManualSettingSwitcher){#if not launched from parameters
						Write-Host "  Or 按下 '$X[38;2;255;165;000;22mc$X[0m' 取消: " -nonewline
						$SettingsCancelOption = "c"
					}
					$SettingsChoice = ReadKeyTimeout "" $MenuRefreshRate "c" -AdditionalAllowedKeys 27 #$MenuRefreshRate represents the refresh rate of the menu in seconds (30). If no button is pressed, send "c" for cancel.
					Write-Host
					if ($SettingsChoice.tostring() -notin $SettingsFileOptions.id + $SettingsCancelOption + "Esc"){
						Write-Host "  无效输入. 请输入以上选项之一." -foregroundcolor red
						$SettingsChoice = ""
					}
				} until ($SettingsChoice.tostring() -in $SettingsFileOptions.id + $SettingsCancelOption + "Esc")
				if ($SettingsChoice -ne "c" -and $SettingsChoice -ne "Esc"){
					$SettingsToLoadFrom = $SettingsFileOptions | where-object {$_.id -eq $SettingsChoice.tostring()}
					try {
						Copy-item ($SettingsProfilePath + $SettingsToLoadFrom.FileName) -Destination $SettingsJSON #-ErrorAction Stop #overwrite settings.json with settings<Name>.json (<Name> being the name of the config user selects). This means any changes to settings in settings.json will be lost the next time an account is loaded by the script.
						$CurrentLabel = ($Script:AccountOptionsCSV | where-object {$_.id -eq $Script:AccountID}).accountlabel
						Write-Host (" 自定义游戏设置 (" + $SettingsToLoadFrom.Name + ") 用于 " + $CurrentLabel + "`n") -foregroundcolor green
						Start-Sleep -milliseconds 100
					}
					catch {
						FormatFunction -Text "无法覆盖 settings.json 因为某些原因. 确保你没有打开文件!" -IsError
						PressTheAnyKey
					}
				}
			}
			Else {# if no custom settings files are found, IE user hasn't set them up yet.
				Write-Host "`n  没有自定义设置文件已保存. 加载默认设置." -foregroundcolor Yellow
				Write-Host "  查看 README 获取设置说明.`n" -foregroundcolor Yellow
				PressTheAnyKey
			}
		}
		if ($SettingsChoice -ne "c" -and $SettingsChoice -ne "Esc"){
			#Start Game
			KillHandle | out-null # Quickly check that there's no open handles already for "DiabloII Check For Other Instances"
			$arguments += " --instance$($Script:AccountChoice.ID)"
			if ($Script:Config.DisableIconStacking -eq $True){
				$ShortcutPath = "$Script:WorkingDirectory\D2r_Instance$($Script:AccountChoice.ID).lnk"
				$__GamePathLaunch = if ($isCN -and $Script:GamePathCN) { $Script:GamePathCN } else { $Script:GamePath }
				CreateShortcut -shortcutPath $ShortcutPath -targetPath "$__GamePathLaunch\D2R.exe" -arguments $arguments
				Start-Process -FilePath $ShortcutPath
				Start-Sleep -milliseconds 1500 #give D2r a bit of a chance to start up before trying to kill handle
				Remove-Item -Path $ShortcutPath -Force
			}
			Else {
				$__GamePathLaunch = if ($isCN -and $Script:GamePathCN) { $Script:GamePathCN } else { $Script:GamePath }
				Start-Process "$__GamePathLaunch\D2R.exe" -ArgumentList "$arguments"
				Start-Sleep -milliseconds 1 #give D2r a bit of a chance to start up before trying to kill handle
			}
			$process = Get-CimInstance -ClassName Win32_Process | Where-Object {
				$_.Name -eq "D2R.exe" -and $_.CommandLine -match "--instance$($Script:AccountChoice.ID)"
			}
			#Close the 'Check for other instances' handle
			Write-Host " 尝试关闭 `"Check for other instances`" 句柄..."
			$Output = KillHandle | out-string #run KillHandle function.
			if (($Output.contains("DiabloII Check For Other Instances")) -eq $true){
				$handlekilled = $true
				Write-Host " `"Check for Other Instances`" 句柄已关闭." -foregroundcolor green
			}
			else {
				$Output = KillHandle -TryAgain | out-string #Try again 3 more times with small delays in case game is taking a while to load.
				if (($Output.contains("DiabloII Check For Other Instances")) -eq $true){
					$handlekilled = $true
					Write-Host " `"Check for Other Instances`" 句柄已关闭." -foregroundcolor green
				}
			}
			if ($handlekilled -ne $True){
				FormatFunction -Text "无法关闭 `"Check for other instances`" 句柄 或 找不到该句柄." -IsError -Indents 1
				Write-Host " 游戏可能没有按预期启动." -foregroundcolor red
				PressTheAnyKey
			}
			#Rename the Diablo Game window for easier identification of which account and region the game is.
			$rename = ($Script:AccountID + " - " + $Script:AccountFriendlyName + " (" + $Script:Region + ")" + " - Diablo II: Resurrected")
			$Command = ('"'+ $WorkingDirectory + '\SetText\SetTextv2.exe" /PID ' + $process.ProcessID + ' "' + $rename + '"')
			try {
				cmd.exe /c $Command
				write-debug $Command #debug
				Write-debug " Window Renamed." #debug
				Start-Sleep -milliseconds 250
			}
			catch {
				Write-Host " Couldn't rename window :(" -foregroundcolor red
				PressTheAnyKey
			}
			If ($Script:Config.RememberWindowLocations -eq $True -and $Script:ParamLaunchAndAccountNotInAccountsCSV -ne $True){ #If user has enabled the feature to automatically move game Windows to preferred screen locations.
				if ($Script:AccountChoice.WindowXCoordinates -ne "" -and $Script:AccountChoice.WindowYCoordinates -ne "" -and $Null -ne $Script:AccountChoice.WindowXCoordinates -and $Null -ne $Script:AccountChoice.WindowYCoordinates -and $Script:AccountChoice.WindowWidth -ne "" -and $Script:AccountChoice.WindowHeight -ne "" -and $Null -ne $Script:AccountChoice.WindowWidth -and $Null -ne $Script:AccountChoice.WindowHeight){ #Check if the account has had coordinates saved yet.
					$GetLoadWindowClassFunc = $(Get-Command LoadWindowClass).Definition
					$GetSetWindowLocationsFunc = $(Get-Command SetWindowLocations).Definition
					$JobID = (Start-Job -ScriptBlock { # Run this in a background job so we don't have to wait for it to complete
						start-sleep -milliseconds 2024 # We need to wait for about 2 seconds for game to load as if we move it too early, the game itself will reposition the window. Absolute minimum is 420 milliseconds (funnily enough). Delay may need to be a bit higher for people with wooden computers.
						Invoke-Expression "function LoadWindowClass {$using:GetLoadWindowClassFunc}"
						Invoke-Expression "function SetWindowLocations {$using:GetSetWindowLocationsFunc}"
						SetWindowLocations -x $Using:AccountChoice.WindowXCoordinates -y $Using:AccountChoice.WindowYCoordinates -Width $Using:AccountChoice.WindowWidth -height $Using:AccountChoice.WindowHeight -Id $Using:process.ProcessID
					}).id
					$Script:MovedWindowLocations ++
					$Script:JobIDs += $JobID
				}
				Else { #Show a warning if user has RememberWindowLocations but hasn't configured it for this account yet.
					FormatFunction -iswarning -text "`n 我发现你已打开配置 'RememberWindowLocations'（窗口记忆功能） ，但目前尚未保存任何坐标。`n`n可按照以下步骤快速保存一份："
					FormatFunction -iswarning -indents 1 -SubsequentLineIndents 3 -text "1. 打开 D2R 账号游戏窗口。`n2. 将游戏窗口移动到你喜欢的布局。"
					FormatFunction -iswarning -indents 1 -SubsequentLineIndents 3 -text "3. 进入工具主菜单，进入 'RememberWindowLocations' 设置。`n4. 进入此菜单后，选择 's' 选项，保存任何打开的游戏实例的坐标。"	
					FormatFunction -iswarning -text  "`n如此，当你再次打开这些账号时，它们都被初始化在记忆的屏幕位置`n"
					if ($Script:AccountChoice.AuthenticationMethod -ne "Token"){ #If a token is used we can't pause it as it the script will not detect when char select screen is reached. 1.13.1 hotfix
						PressTheAnyKey
					}
				}
			}
			if ($Script:AccountChoice.AuthenticationMethod -eq "Token" -or (($Script:ForceAuthToken -eq $True -or $Script:Config.ForceAuthTokenForRegion -match $RegionLabel) -and $Script:AccountChoice.Token.length -ge 200)){#wait for web_token to change twice (once for launch, once for char select screen, before being able to launch additional accounts. Token will have already changed once by the time script reaches this stage
				$CurrentTokenRegValue = (Get-ItemProperty -Path $Path -Name WEB_TOKEN).WEB_TOKEN
				Write-Host " 正在使用 $X[38;2;165;146;99;4m令牌模式$X[0m 启动游戏"
				Write-Host " 等待本实例游戏启动并进入选人界面..." -foregroundcolor yellow
				Write-Host " $X[38;2;255;255;0;4m ⚠ ⚠ 请在进入选人界面之前，不要启动或切换到任何其他游戏窗口！ ⚠ ⚠ .$X[0m"
				do {
					$NewTokenRegValue = (Get-ItemProperty -Path $Path -Name WEB_TOKEN).WEB_TOKEN
					$CompareCheck = Compare-Object $CurrentTokenRegValue $NewTokenRegValue
					if ($Null -ne $CompareCheck){#if CompareCheck has some value, this means it found differences, IE the reg value changed.
						$CurrentTokenRegValue = (Get-ItemProperty -Path $Path -Name WEB_TOKEN).WEB_TOKEN
						$CurrentTokenRegValue = $NewTokenRegValue
						$WebTokenChangeCounter++
						Write-Host " $Script:AccountFriendlyName 的游戏实例已成功启动！" -foregroundcolor green
					}
					else {
						Start-Sleep -milliseconds 451
					}
				} until ($WebTokenChangeCounter -eq 1)
			}
			if ($Script:LastAccount -eq $True -or ($Script:OpenAllAccounts -ne $True -and $Script:OpenBatches -ne $True)){
				if ($Script:MovedWindowLocations -ge 1){
					if ($Script:MovedWindowLocations -gt 1){
						$plural = "s"
					}
					FormatFunction -IsSuccess -text "将游戏窗口移动到$plural 预设位置$plural."
					Start-Sleep -milliseconds 750
				}
				Write-Host "`n Good luck hero..." -foregroundcolor magenta
			}
			Start-Sleep -milliseconds 1000
			$Script:ScriptHasBeenRun = $true
		}
	}
}
if ($TestTTS -or ($null -ne $TestVoice -and $TestVoice -ne "")) {
    Write-Host "进入 TTS 测试模式 (跳过网络请求)... TestTTS=$TestTTS; TestVoice='$TestVoice'" -ForegroundColor Yellow
    $pref = $null
    try { $pref = if ($null -ne $TestVoice -and $TestVoice -ne "") { $TestVoice } elseif ($Script:Config -and $Script:Config.DCloneAlarmVoice) { $Script:Config.DCloneAlarmVoice } else { $null } } catch { $pref = $TestVoice }
    Test-DCloneTTS -Preference $pref
    Exit
}
RemoveMaximiseButton #移除 PowerShell 窗口中的最大化按钮.
InitialiseCurrentStats #Get Stats
ImportXML #Get your config
#获取 D2Emu 连接信息. 需要 TZ/DClone 数据. 添加半秒到启动时间.
if ($Script:ParamsUsed -ne $True){#不要获取 D2Emu 连接信息如果脚本参数正在使用.
	GetEmuToken
}
ValidationAndSetup #我做了一些不同版本的更改, 这里检查任何需要做的配置更新并帮助防止用户错误 (Typo's)
ImportCSV #获取账号和账号统计.
Clear-Host #清除屏幕上的所有初始化垃圾.
D2rLevels #Level ID's used for comparing TZ data against.
QuoteList #List of D2 quotes to display.
SetQualityRolls #Randomly roll quotes in D2 themed colours.
Menu #start script.

#For Diablo II: Resurrected
#Dedicated to my cat Toby.

#Text Colors
#Color Hex	RGB Value	Description
#FFA500		255 165 000	Crafted items
#4169E1		065 105 225	Magic items
#FFFF00		255 255 000	Rare items
#00FF00		000 255 000	Set items
#A59263		165 146 099	Unique items